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

Merge pull request #13038 from overleaf/ii-history-react-dropdowns

[web] Adjust dropdowns placement in history list

GitOrigin-RevId: 050e16b97630eb37c890ad9a97a56ba882cca06b
ilkin-overleaf 3 лет назад
Родитель
Сommit
32160a8c80

+ 4 - 1
services/web/frontend/js/features/history/components/change-list/change-list.tsx

@@ -14,7 +14,10 @@ function ChangeList() {
         )}
       </div>
       {!error && (
-        <div className="history-version-list-container">
+        <div
+          className="history-version-list-container"
+          data-history-version-list-container
+        >
           {labelsOnly ? <LabelsList /> : <AllHistoryList />}
         </div>
       )}

+ 51 - 24
services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx

@@ -1,42 +1,69 @@
-import { useState } from 'react'
+import { useState, useRef, useEffect } from 'react'
 import { useTranslation } from 'react-i18next'
 import { Dropdown } from 'react-bootstrap'
 import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip'
 import Icon from '../../../../../shared/components/icon'
+import DropdownMenuWithRef from '../../../../../shared/components/dropdown/dropdown-menu-with-ref'
 
 type DropdownMenuProps = {
   id: string
   children: React.ReactNode
+  parentSelector?: string
 }
 
-function ActionsDropdown({ id, children }: DropdownMenuProps) {
+function ActionsDropdown({ id, children, parentSelector }: DropdownMenuProps) {
   const { t } = useTranslation()
   const [isOpened, setIsOpened] = useState(false)
+  const menuRef = useRef<HTMLElement>()
+
+  // handle the placement of the dropdown above or below the toggle button
+  useEffect(() => {
+    if (menuRef.current && parentSelector) {
+      const parent = menuRef.current.closest(parentSelector)
+
+      if (!parent) {
+        return
+      }
+
+      const parentBottom = parent.getBoundingClientRect().bottom
+      const { top, height } = menuRef.current.getBoundingClientRect()
+
+      if (top + height > parentBottom) {
+        menuRef.current.style.bottom = '100%'
+        menuRef.current.style.top = 'auto'
+      } else {
+        menuRef.current.style.bottom = 'auto'
+        menuRef.current.style.top = '100%'
+      }
+    }
+  })
 
   return (
-    <>
-      <Dropdown
-        id={`history-version-dropdown-${id}`}
-        pullRight
-        open={isOpened}
-        onToggle={open => setIsOpened(open)}
+    <Dropdown
+      id={`history-version-dropdown-${id}`}
+      pullRight
+      open={isOpened}
+      onToggle={open => setIsOpened(open)}
+    >
+      <DropdownToggleWithTooltip
+        bsRole="toggle"
+        className="history-version-dropdown-menu-btn"
+        tooltipProps={{
+          id,
+          description: t('more_actions'),
+          overlayProps: { placement: 'bottom', trigger: ['hover'] },
+        }}
+      >
+        <Icon type="ellipsis-v" accessibilityLabel={t('more_actions')} />
+      </DropdownToggleWithTooltip>
+      <DropdownMenuWithRef
+        bsRole="menu"
+        className="history-version-dropdown-menu"
+        menuRef={menuRef}
       >
-        <DropdownToggleWithTooltip
-          bsRole="toggle"
-          className="history-version-dropdown-menu-btn"
-          tooltipProps={{
-            id,
-            description: t('more_actions'),
-            overlayProps: { placement: 'bottom', trigger: ['hover'] },
-          }}
-        >
-          <Icon type="ellipsis-v" accessibilityLabel={t('more_actions')} />
-        </DropdownToggleWithTooltip>
-        <Dropdown.Menu className="history-version-dropdown-menu">
-          {children}
-        </Dropdown.Menu>
-      </Dropdown>
-    </>
+        {children}
+      </DropdownMenuWithRef>
+    </Dropdown>
   )
 }
 

+ 4 - 1
services/web/frontend/js/features/history/components/change-list/dropdown/history-version-dropdown.tsx

@@ -22,7 +22,10 @@ function HistoryVersionDropdown({
   updateMetaEndTimestamp,
 }: HistoryVersionDropdownProps) {
   return (
-    <ActionsDropdown id={id}>
+    <ActionsDropdown
+      id={id}
+      parentSelector="[data-history-version-list-container]"
+    >
       <AddLabel projectId={projectId} version={toV} />
       <Download projectId={projectId} version={toV} />
       {!isComparing && !isSelected && (

+ 4 - 1
services/web/frontend/js/features/history/components/change-list/dropdown/label-dropdown.tsx

@@ -20,7 +20,10 @@ function LabelDropdown({
   updateMetaEndTimestamp,
 }: LabelDropdownProps) {
   return (
-    <ActionsDropdown id={id}>
+    <ActionsDropdown
+      id={id}
+      parentSelector="[data-history-version-list-container]"
+    >
       <Download projectId={projectId} version={version} />
       {!isComparing && !isSelected && (
         <Compare

+ 72 - 0
services/web/frontend/js/shared/components/dropdown/dropdown-menu-with-ref.tsx

@@ -0,0 +1,72 @@
+import { forwardRef } from 'react'
+import classnames from 'classnames'
+// @ts-ignore
+// eslint-disable-next-line import/no-extraneous-dependencies
+import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'
+import { DropdownProps } from 'react-bootstrap'
+import { MergeAndOverride } from '../../../../../types/utils'
+
+type DropdownMenuWithRefProps = MergeAndOverride<
+  Pick<DropdownProps, 'bsClass' | 'open' | 'pullRight' | 'onClose'>,
+  {
+    children: React.ReactNode
+    bsRole: 'menu'
+    menuRef: React.MutableRefObject<HTMLElement | undefined>
+    className?: string
+    // The props below are passed by react-bootstrap
+    labelledBy?: string | undefined
+    rootCloseEvent?: 'click' | 'mousedown' | undefined
+  }
+>
+
+const DropdownMenuWithRef = forwardRef<
+  HTMLUListElement,
+  DropdownMenuWithRefProps
+>(function (props, ref) {
+  const {
+    children,
+    bsRole,
+    bsClass,
+    className,
+    open,
+    pullRight,
+    labelledBy,
+    menuRef,
+    onClose,
+    rootCloseEvent,
+    ...rest
+  } = props
+
+  // expose the menu reference to both the `menuRef` and `ref callback` from react-bootstrap
+  const handleRefs = (node: HTMLUListElement) => {
+    if (typeof ref === 'function') {
+      ref(node)
+    }
+    menuRef.current = node
+  }
+
+  // Implementation as suggested in
+  // https://react-bootstrap-v3.netlify.app/components/dropdowns/#btn-dropdowns-props-dropdown
+  return (
+    <RootCloseWrapper
+      disabled={!open}
+      onRootClose={onClose}
+      event={rootCloseEvent}
+    >
+      <ul
+        role={bsRole}
+        className={classnames(className, bsClass, {
+          'dropdown-menu-right': pullRight,
+        })}
+        aria-labelledby={labelledBy}
+        ref={handleRefs}
+        {...rest}
+      >
+        {children}
+      </ul>
+    </RootCloseWrapper>
+  )
+})
+DropdownMenuWithRef.displayName = 'DropdownMenuWithRef'
+
+export default DropdownMenuWithRef

+ 4 - 1
services/web/frontend/js/shared/components/dropdown/dropdown-toggle-with-tooltip.tsx

@@ -5,7 +5,7 @@ import { DropdownProps } from 'react-bootstrap'
 import { MergeAndOverride } from '../../../../../types/utils'
 
 type CustomToggleProps = MergeAndOverride<
-  Pick<DropdownProps, 'bsClass'>,
+  Pick<DropdownProps, 'bsClass' | 'open'>,
   {
     children: React.ReactNode
     bsRole: 'toggle'
@@ -23,6 +23,7 @@ const DropdownToggleWithTooltip = forwardRef<
     children,
     bsClass,
     className,
+    open,
     bsRole: _bsRole,
     ...rest
   } = props
@@ -33,6 +34,8 @@ const DropdownToggleWithTooltip = forwardRef<
         type="button"
         ref={ref}
         className={classnames(bsClass, 'btn', className)}
+        aria-expanded={open}
+        aria-haspopup="true"
         {...rest}
       >
         {children}