Merge pull request #23300 from overleaf/mj-ide-menu-hover

[web] Introduce menu bar shared component

GitOrigin-RevId: c304cc4e1e5961fe4ef7d2112e8d9f91c47dd0ec
This commit is contained in:
David
2025-02-06 15:41:18 +00:00
committed by Copybot
parent b5c11370d6
commit bd76193eb5
10 changed files with 199 additions and 45 deletions

View File

@@ -1,32 +1,29 @@
import {
Dropdown,
DropdownDivider,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
import { FC } from 'react'
import { DropdownDivider } 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 { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
import { useTranslation } from 'react-i18next'
type MenuBarOptionProps = {
title: string
onClick?: () => void
}
type MenuBarDropdownProps = {
title: string
id: string
}
export const ToolbarMenuBar = () => {
const { t } = useTranslation()
return (
<div className="ide-redesign-toolbar-menu-bar">
<MenuBarDropdown title={t('file')} id="file">
<MenuBar
className="ide-redesign-toolbar-menu-bar"
id="toolbar-menu-bar-item"
>
<MenuBarDropdown
title={t('file')}
id="file"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
<MenuBarOption title="New File" />
<MenuBarOption title="New Project" />
</MenuBarDropdown>
<MenuBarDropdown title={t('edit')} id="edit">
<MenuBarDropdown
title={t('edit')}
id="edit"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
<MenuBarOption title="Undo" />
<MenuBarOption title="Redo" />
<DropdownDivider />
@@ -34,44 +31,41 @@ export const ToolbarMenuBar = () => {
<MenuBarOption title="Copy" />
<MenuBarOption title="Pate" />
</MenuBarDropdown>
<MenuBarDropdown title={t('view')} id="view">
<MenuBarDropdown
title={t('view')}
id="view"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
<MenuBarOption title="PDF only" />
</MenuBarDropdown>
<MenuBarDropdown title={t('insert')} id="insert">
<MenuBarDropdown
title={t('insert')}
id="insert"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
<MenuBarOption title="Insert figure" />
<MenuBarOption title="Insert table" />
<MenuBarOption title="Insert link" />
<MenuBarOption title="Add comment" />
</MenuBarDropdown>
<MenuBarDropdown title={t('format')} id="format">
<MenuBarDropdown
title={t('format')}
id="format"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
<MenuBarOption title="Bold text" />
</MenuBarDropdown>
<MenuBarDropdown title={t('help')} id="help">
<MenuBarDropdown
title={t('help')}
id="help"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
<MenuBarOption title="Keyboard shortcuts" />
<MenuBarOption title="Documentation" />
<DropdownDivider />
<MenuBarOption title="Contact us" />
<MenuBarOption title="Give feedback" />
</MenuBarDropdown>
</div>
</MenuBar>
)
}
const MenuBarDropdown: FC<MenuBarDropdownProps> = ({ title, children, id }) => {
return (
<Dropdown align="start">
<DropdownToggle
id={`toolbar-menu-bar-item-${id}`}
variant="secondary"
className="ide-redesign-toolbar-dropdown-toggle-subdued"
>
{title}
</DropdownToggle>
<DropdownMenu>{children}</DropdownMenu>
</Dropdown>
)
}
const MenuBarOption = ({ title, onClick }: MenuBarOptionProps) => {
return <OLDropdownMenuItem onClick={onClick}>{title}</OLDropdownMenuItem>
}

View File

@@ -57,6 +57,7 @@ export type DropdownToggleProps = PropsWithChildren<{
size?: 'sm' | 'lg' | undefined
tabIndex?: number
'aria-label'?: string
onMouseEnter?: React.MouseEventHandler
}>
export type DropdownMenuProps = PropsWithChildren<{
@@ -66,6 +67,7 @@ export type DropdownMenuProps = PropsWithChildren<{
className?: string
flip?: boolean
id?: string
renderOnMount?: boolean
}>
export type DropdownDividerProps = PropsWithChildren<{

View File

@@ -0,0 +1,53 @@
import {
Dropdown,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { FC, useCallback } from 'react'
import classNames from 'classnames'
import { useMenuBar } from '@/shared/hooks/use-menu-bar'
type MenuBarDropdownProps = {
title: string
id: string
className?: string
}
export const MenuBarDropdown: FC<MenuBarDropdownProps> = ({
title,
children,
id,
className,
}) => {
const { menuId, selected, setSelected } = useMenuBar()
const onToggle = useCallback(
show => {
setSelected(show ? id : null)
},
[id, setSelected]
)
const onHover = useCallback(() => {
setSelected(prev => {
if (prev === null) {
return null
}
return id
})
}, [id, setSelected])
return (
<Dropdown show={selected === id} align="start" onToggle={onToggle}>
<DropdownToggle
id={`${menuId}-${id}`}
variant="secondary"
className={classNames(className, 'menu-bar-toggle')}
onMouseEnter={onHover}
>
{title}
</DropdownToggle>
<DropdownMenu renderOnMount>{children}</DropdownMenu>
</Dropdown>
)
}

View File

@@ -0,0 +1,15 @@
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
type MenuBarOptionProps = {
title: string
onClick?: () => void
}
export const MenuBarOption = ({ title, onClick }: MenuBarOptionProps) => {
return (
<DropdownListItem>
<DropdownItem onClick={onClick}>{title}</DropdownItem>
</DropdownListItem>
)
}

View File

@@ -0,0 +1,17 @@
import { MenuBarContext } from '@/shared/context/menu-bar-context'
import { FC, HTMLProps, useState } 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 }}>
{children}
</MenuBarContext.Provider>
</div>
)
}

View File

@@ -0,0 +1,11 @@
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,10 @@
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,38 @@
import { DropdownDivider } 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 { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
import { Meta } from '@storybook/react/*'
export const Default = () => {
return (
<MenuBar id="toolbar-menu-bar-item">
<MenuBarDropdown title="File" id="file">
<MenuBarOption title="New File" />
<MenuBarOption title="New Project" />
</MenuBarDropdown>
<MenuBarDropdown title="Edit" id="edit">
<MenuBarOption title="Undo" />
<MenuBarOption title="Redo" />
<DropdownDivider />
<MenuBarOption title="Cut" />
<MenuBarOption title="Copy" />
<MenuBarOption title="Paste" />
</MenuBarDropdown>
<MenuBarDropdown title="View" id="view">
<MenuBarOption title="PDF only" />
</MenuBarDropdown>
</MenuBar>
)
}
const meta: Meta<typeof MenuBar> = {
title: 'Shared / Components / MenuBar',
component: MenuBar,
argTypes: {},
parameters: {
bootstrap5: true,
},
}
export default meta

View File

@@ -37,3 +37,4 @@
@import 'tos';
@import 'collapsible-file-header';
@import 'panel-heading';
@import 'menu-bar';

View File

@@ -0,0 +1,13 @@
.menu-bar-toggle {
border: none;
border-radius: var(--border-radius-base);
padding: var(--spacing-02);
font-size: var(--font-size-03);
line-height: var(--line-height-03);
font-weight: 400;
box-sizing: border-box;
&.dropdown-toggle::after {
display: none;
}
}