diff --git a/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx b/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx index 916e97ec806a..a19243a34be6 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx @@ -19,7 +19,7 @@ import type { FunctionType, ISequenceNode } from '@univerjs/engine-formula'; import { CommandType, DisposableCollection, ICommandService } from '@univerjs/core'; import { borderClassName, clsx, scrollbarClassName } from '@univerjs/design'; import { DeviceInputEventType } from '@univerjs/engine-render'; -import { IShortcutService, KeyCode, RectPopup, useDependency } from '@univerjs/ui'; +import { IShortcutService, KeyCode, RectPopup, useDependency, useVirtualList } from '@univerjs/ui'; import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import { useEditorPosition } from '../hooks/use-editor-position'; import { useFormulaSearch } from '../hooks/use-formula-search'; @@ -45,12 +45,18 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { const commandService = useDependency(ICommandService); const { searchList, searchText, handlerFormulaReplace, reset: resetFormulaSearch } = useFormulaSearch(isFocus, sequenceNodes, editor); const visible = useMemo(() => !!searchList.length, [searchList]); - const ulRef = useRef(undefined); + const containerRef = useRef(null); const [active, setActive] = useState(0); const isEnableMouseEnterOrOut = useRef(false); const [position$] = useEditorPosition(editorId, visible, [searchText, searchList]); const stateRef = useStateRef({ searchList, active }); + const [virtualList, { wrapperStyle, scrollTo, containerProps }] = useVirtualList(searchList, { + containerTarget: containerRef, + itemHeight: 40, + overscan: 5, + }); + const handleFunctionSelect = (v: string, functionType: FunctionType) => { const res = handlerFormulaReplace(v, functionType); if (res) { @@ -144,36 +150,7 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { }, [searchList]); function scrollToVisible(liIndex: number) { - // Get the
  • element directly from children - const ulElement = ulRef.current; - if (!ulElement) return; - - const liElement = ulElement.children[liIndex] as HTMLLIElement; - if (!liElement) return; - - // Get the height of the
      element - const ulRect = ulElement.getBoundingClientRect(); - const ulTop = ulRect.top; - const ulHeight = ulElement.offsetHeight; - - // Get the position and height of the
    • element - const liRect = liElement.getBoundingClientRect(); - const liTop = liRect.top; - const liHeight = liRect.height; - - // If the
    • element is within the visible area, no scrolling operation is performed - if (liTop >= 0 && liTop > ulTop && liTop - ulTop + liHeight <= ulHeight) { - return; - } - - // Calculate scroll position - const scrollTo = liElement.offsetTop - (ulHeight - liHeight) / 2; - - // Perform scrolling operation - ulElement.scrollTo({ - top: scrollTo, - behavior: 'smooth', - }); + scrollTo(liIndex); } const debounceResetMouseState = useMemo(() => { @@ -191,11 +168,12 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
        { - ulRef.current = v!; + containerRef.current = v; if (ref) { - ref.current = v!; + ref.current = v; } }} + {...containerProps} data-u-comp="sheets-formula-editor" className={clsx(` univer-m-0 univer-box-border univer-max-h-[400px] univer-w-[250px] univer-list-none @@ -204,37 +182,39 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { dark:!univer-bg-gray-900 `, borderClassName, scrollbarClassName)} > - {searchList.map((item, index) => ( -
      • handleLiMouseEnter(index)} - onMouseLeave={handleLiMouseLeave} - onMouseMove={debounceResetMouseState} - onClick={() => { - handleFunctionSelect(item.name, item.functionType); - if (editor) { - editor.focus(); - } - }} - > - - {item.name.substring(0, searchText.length)} - {item.name.slice(searchText.length)} - - + {virtualList.map(({ data: item, index }) => ( +
      • handleLiMouseEnter(index)} + onMouseLeave={handleLiMouseLeave} + onMouseMove={debounceResetMouseState} + onClick={() => { + handleFunctionSelect(item.name, item.functionType); + if (editor) { + editor.focus(); + } + }} > - {item.desc} - -
      • - ))} + + {item.name.substring(0, searchText.length)} + {item.name.slice(searchText.length)} + + + {item.desc} + + + ))} +
      ); diff --git a/packages/sheets-formula-ui/src/views/more-functions/select-function/SelectFunction.tsx b/packages/sheets-formula-ui/src/views/more-functions/select-function/SelectFunction.tsx index 293aebd9b386..06d391ffb989 100644 --- a/packages/sheets-formula-ui/src/views/more-functions/select-function/SelectFunction.tsx +++ b/packages/sheets-formula-ui/src/views/more-functions/select-function/SelectFunction.tsx @@ -22,8 +22,8 @@ import { IConfigService, LocaleService } from '@univerjs/core'; import { borderClassName, clsx, Input, scrollbarClassName, Select } from '@univerjs/design'; import { CheckMarkIcon } from '@univerjs/icons'; import { IDescriptionService, PLUGIN_CONFIG_KEY_BASE } from '@univerjs/sheets-formula'; -import { ISidebarService, useDependency, useObservable } from '@univerjs/ui'; -import { useEffect, useState } from 'react'; +import { ISidebarService, useDependency, useObservable, useVirtualList } from '@univerjs/ui'; +import { useEffect, useRef, useState } from 'react'; import { getFunctionTypeValues } from '../../../services/utils'; import { FunctionHelp } from '../function-help/FunctionHelp'; import { FunctionParams } from '../function-params/FunctionParams'; @@ -50,6 +50,13 @@ export function SelectFunction(props: ISelectFunctionProps) { const sidebarService = useDependency(ISidebarService); const sidebarOptions = useObservable(sidebarService.sidebarOptions$); + const containerRef = useRef(null); + const [virtualList, { wrapperStyle, containerProps }] = useVirtualList(selectList, { + containerTarget: containerRef, + itemHeight: 32, + overscan: 5, + }); + const options = getFunctionTypeValues(localeService, Boolean(customFunction)) .filter( (option) => descriptionService.getSearchListByType(Number(option.value)).length > 0 @@ -172,6 +179,8 @@ export function SelectFunction(props: ISelectFunctionProps) { {selectList.length > 0 && (
        - {selectList.map(({ name }, index) => ( -
      • handleLiMouseEnter(index)} - onMouseLeave={handleLiMouseLeave} - onClick={() => setCurrentFunctionInfo(index)} - > - {nameSelected === index && ( - - )} - {highlightSearchText(name)} -
      • - ))} +
        + {virtualList.map(({ data: { name }, index }) => ( +
      • handleLiMouseEnter(index)} + onMouseLeave={handleLiMouseLeave} + onClick={() => setCurrentFunctionInfo(index)} + > + {nameSelected === index && ( + + )} + {highlightSearchText(name)} +
      • + ))} +
      )} diff --git a/packages/sheets-ui/src/views/sheet-bar/sheet-bar-tabs/SheetBarItem.tsx b/packages/sheets-ui/src/views/sheet-bar/sheet-bar-tabs/SheetBarItem.tsx index 1aa638ac3d14..50a5c695d0ec 100644 --- a/packages/sheets-ui/src/views/sheet-bar/sheet-bar-tabs/SheetBarItem.tsx +++ b/packages/sheets-ui/src/views/sheet-bar/sheet-bar-tabs/SheetBarItem.tsx @@ -19,6 +19,7 @@ import type { CSSProperties, KeyboardEventHandler, ReactNode } from 'react'; import { ColorKit, ThemeService } from '@univerjs/core'; import { clsx } from '@univerjs/design'; import { useDependency } from '@univerjs/ui'; +import { memo } from 'react'; export interface IBaseSheetBarProps { label?: ReactNode; @@ -35,7 +36,7 @@ export interface IBaseSheetBarProps { tabIndex?: number; } -export function SheetBarItem(props: IBaseSheetBarProps) { +export const SheetBarItem = memo(function SheetBarItem(props: IBaseSheetBarProps) { const { sheetId, label, color, selected, className, onKeyDown, tabIndex } = props; const themeService = useDependency(ThemeService); @@ -87,4 +88,4 @@ export function SheetBarItem(props: IBaseSheetBarProps) { ); -} +}); diff --git a/packages/ui/src/components/hooks/virtual-list.ts b/packages/ui/src/components/hooks/virtual-list.ts index 6fa42536d201..894ddf735bed 100644 --- a/packages/ui/src/components/hooks/virtual-list.ts +++ b/packages/ui/src/components/hooks/virtual-list.ts @@ -15,6 +15,7 @@ */ import type { Nullable } from '@univerjs/core'; +import type { RefObject } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useEvent } from './event'; @@ -23,7 +24,7 @@ type ItemHeight = (index: number, data: T) => number; const isNumber = (value: unknown): value is number => typeof value === 'number'; export interface IVirtualListOptions { - containerTarget: React.RefObject; + containerTarget: RefObject; itemHeight: number | ItemHeight; overscan?: number; } diff --git a/packages/ui/src/views/components/context-menu/ContextMenuPanel.tsx b/packages/ui/src/views/components/context-menu/ContextMenuPanel.tsx index f6b1ce557907..e3917cf4a7d7 100644 --- a/packages/ui/src/views/components/context-menu/ContextMenuPanel.tsx +++ b/packages/ui/src/views/components/context-menu/ContextMenuPanel.tsx @@ -26,10 +26,11 @@ import type { IMenuSchema } from '../../../services/menu/menu-manager.service'; import { isRealNum, LocaleService } from '@univerjs/core'; import { borderBottomClassName, borderClassName, clsx, scrollbarClassName } from '@univerjs/design'; import { CheckMarkIcon, MoreIcon } from '@univerjs/icons'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { combineLatest, isObservable, of, scan, startWith } from 'rxjs'; import { CustomLabel } from '../../../components/custom-label/CustomLabel'; +import { useVirtualList } from '../../../components/hooks'; import { useScrollYOverContainer } from '../../../components/hooks/layout'; import { UIQuickTileMenuGroup, UITinyMenuGroup } from '../../../components/menu/desktop/TinyMenuGroup'; import { ILayoutService } from '../../../services/layout/layout.service'; @@ -265,7 +266,7 @@ function ContextMenuMenu(props: IContextMenuMenuProps) { ); } -function ContextMenuMenuItem(props: IContextMenuMenuItemProps) { +const ContextMenuMenuItem = memo(function ContextMenuMenuItem(props: IContextMenuMenuItemProps) { const { menuKey, menuItem, submenuPortalContainer, onOptionSelect, maxMenuHeight } = props; const menuManagerService = useDependency(IMenuManagerService); const disabled = useObservable(menuItem.disabled$, false); @@ -302,6 +303,13 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) { return Array.isArray(selectorItem.selections) ? selectorItem.selections : []; }, [menuItem.type, selectionsFromObservable, selectorItem.selections]); + const selectionsContainerRef = useRef(null); + const [virtualList, { wrapperStyle, containerProps }] = useVirtualList(selections, { + containerTarget: selectionsContainerRef, + itemHeight: 36, + overscan: 5, + }); + const subMenuItems = useMemo(() => { if (menuItem.type !== MenuItemType.SUBITEMS || !menuItem.id) { return []; @@ -513,6 +521,8 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) { onWheel={(event) => event.stopPropagation()} >
      {hasSelectionSubmenu && ( -
      - {selections.map((option, index) => { +
      + {virtualList.map(({ data: option, index }) => { const optionKey = `${menuItem.id}-${option.label ?? option.id}-${index}`; const optionSelected = typeof inputValue !== 'undefined' && String(inputValue) === String(option.value); const optionSelectable = !isNonSelectableLabel(option.label); @@ -631,7 +641,7 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) { )}
      ); -} +}); function useContextGroupHiddenStates(menuSchemas: IMenuSchema[]) { const [hiddenStates, setHiddenStates] = useState>({}); diff --git a/packages/ui/src/views/components/ribbon/ToolbarItem.tsx b/packages/ui/src/views/components/ribbon/ToolbarItem.tsx index d84ebe641e73..62c6ea5753c6 100644 --- a/packages/ui/src/views/components/ribbon/ToolbarItem.tsx +++ b/packages/ui/src/views/components/ribbon/ToolbarItem.tsx @@ -19,7 +19,7 @@ import type { ITooltipWrapperRef } from './TooltipButtonWrapper'; import { ICommandService, LocaleService } from '@univerjs/core'; import { clsx } from '@univerjs/design'; import { MoreDownIcon } from '@univerjs/icons'; -import { forwardRef, useMemo } from 'react'; +import { forwardRef, memo, useMemo } from 'react'; import { isObservable, Observable } from 'rxjs'; import { ComponentManager } from '../../../common/component-manager'; import { CustomLabel } from '../../../components/custom-label/CustomLabel'; @@ -30,7 +30,7 @@ import { useToolbarItemStatus } from './hook'; import { ToolbarButton } from './ToolbarButton'; import { DropdownMenuWrapper, TooltipWrapper } from './TooltipButtonWrapper'; -export const ToolbarItem = forwardRef>((props, ref) => { +export const ToolbarItem = memo(forwardRef>((props, ref) => { const localeService = useDependency(LocaleService); const commandService = useDependency(ICommandService); const layoutService = useDependency(ILayoutService); @@ -260,4 +260,4 @@ export const ToolbarItem = forwardRef ); -}); +})); diff --git a/packages/ui/src/views/components/ribbon/TooltipButtonWrapper.tsx b/packages/ui/src/views/components/ribbon/TooltipButtonWrapper.tsx index 5b9cf1bb7570..b122c1c65ad8 100644 --- a/packages/ui/src/views/components/ribbon/TooltipButtonWrapper.tsx +++ b/packages/ui/src/views/components/ribbon/TooltipButtonWrapper.tsx @@ -20,7 +20,17 @@ import type { Subscription } from 'rxjs'; import type { IMenuItem, IValueOption } from '../../../services/menu/menu'; import { clsx, Dropdown, DropdownMenu, Tooltip } from '@univerjs/design'; import { CheckMarkIcon } from '@univerjs/icons'; -import { createContext, forwardRef, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { + createContext, + forwardRef, + memo, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; import { combineLatest, of } from 'rxjs'; import { CustomLabel } from '../../../components/custom-label/CustomLabel'; import { IMenuManagerService } from '../../../services/menu/menu-manager.service'; @@ -114,7 +124,7 @@ export function DropdownWrapper(props: Omit, 'overlay'> ); } -function Label({ icon, value, option, onOptionSelect }: { +const Label = memo(function Label({ icon, value, option, onOptionSelect }: { icon?: IMenuItem['icon']; value?: string | number; option: IValueOption; @@ -147,7 +157,7 @@ function Label({ icon, value, option, onOptionSelect }: { />
      ); -} +}); export function DropdownMenuWrapper({ menuId,