Kaynağa Gözat

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

[web] Introduce menu bar shared component

GitOrigin-RevId: c304cc4e1e5961fe4ef7d2112e8d9f91c47dd0ec
David 1 yıl önce
ebeveyn
işleme
bd76193eb5

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

@@ -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>
-  )
-}
-
-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>
+    </MenuBar>
   )
 }
-
-const MenuBarOption = ({ title, onClick }: MenuBarOptionProps) => {
-  return <OLDropdownMenuItem onClick={onClick}>{title}</OLDropdownMenuItem>
-}

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

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

+ 53 - 0
services/web/frontend/js/shared/components/menu-bar/menu-bar-dropdown.tsx

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

+ 15 - 0
services/web/frontend/js/shared/components/menu-bar/menu-bar-option.tsx

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

+ 17 - 0
services/web/frontend/js/shared/components/menu-bar/menu-bar.tsx

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

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

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

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

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

+ 38 - 0
services/web/frontend/stories/menu-bar.stories.tsx

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

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/all.scss

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

+ 13 - 0
services/web/frontend/stylesheets/bootstrap-5/components/menu-bar.scss

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