Просмотр исходного кода

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

[web] Editor redesign: Make menu bar nestable

GitOrigin-RevId: 5c08126499ff96494d6af9adcbd75126ddd596af
David 1 год назад
Родитель
Сommit
542a52c510

+ 66 - 10
services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx

@@ -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')}

+ 1 - 0
services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts

@@ -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

+ 97 - 5
services/web/frontend/js/shared/components/menu-bar/menu-bar-dropdown.tsx

@@ -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>
   )
 }

+ 5 - 1
services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx

@@ -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>
   )
 }

+ 4 - 5
services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx

@@ -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>
   )
 }

+ 0 - 11
services/web/frontend/js/shared/context/menu-bar-context.tsx

@@ -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
-)

+ 25 - 0
services/web/frontend/js/shared/context/nestable-dropdown-context.tsx

@@ -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>
+  )
+}

+ 0 - 10
services/web/frontend/js/shared/hooks/use-menu-bar.tsx

@@ -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
-}

+ 12 - 0
services/web/frontend/js/shared/hooks/use-nestable-dropdown.tsx

@@ -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
+}

+ 11 - 1
services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss

@@ -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;
+}