Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<HTMLUListElement>(undefined);
const containerRef = useRef<HTMLUListElement>(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) {
Expand Down Expand Up @@ -144,36 +150,7 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
}, [searchList]);

function scrollToVisible(liIndex: number) {
// Get the <li> 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 <ul> element
const ulRect = ulElement.getBoundingClientRect();
const ulTop = ulRect.top;
const ulHeight = ulElement.offsetHeight;

// Get the position and height of the <li> element
const liRect = liElement.getBoundingClientRect();
const liTop = liRect.top;
const liHeight = liRect.height;

// If the <li> 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(() => {
Expand All @@ -191,11 +168,12 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
<RectPopup portal anchorRect$={position$} direction="vertical">
<ul
ref={(v) => {
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
Expand All @@ -204,37 +182,39 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
dark:!univer-bg-gray-900
`, borderClassName, scrollbarClassName)}
>
{searchList.map((item, index) => (
<li
key={item.name}
className={clsx(`
univer-box-border univer-cursor-pointer univer-rounded univer-px-2 univer-py-1
univer-text-gray-900 univer-transition-colors
dark:!univer-text-white
`, {
'univer-bg-gray-200 dark:!univer-bg-gray-600': active === index,
})}
onMouseEnter={() => handleLiMouseEnter(index)}
onMouseLeave={handleLiMouseLeave}
onMouseMove={debounceResetMouseState}
onClick={() => {
handleFunctionSelect(item.name, item.functionType);
if (editor) {
editor.focus();
}
}}
>
<span className="univer-block univer-truncate univer-text-xs">
<span className="univer-text-red-500">{item.name.substring(0, searchText.length)}</span>
<span>{item.name.slice(searchText.length)}</span>
</span>
<span
className="univer-block univer-text-xs univer-text-gray-400"
<div style={wrapperStyle}>
{virtualList.map(({ data: item, index }) => (
<li
key={item.name}
className={clsx(`
univer-box-border univer-cursor-pointer univer-rounded univer-px-2 univer-py-1
univer-text-gray-900 univer-transition-colors
dark:!univer-text-white
`, {
'univer-bg-gray-200 dark:!univer-bg-gray-600': active === index,
})}
onMouseEnter={() => handleLiMouseEnter(index)}
onMouseLeave={handleLiMouseLeave}
onMouseMove={debounceResetMouseState}
onClick={() => {
handleFunctionSelect(item.name, item.functionType);
if (editor) {
editor.focus();
}
}}
>
{item.desc}
</span>
</li>
))}
<span className="univer-block univer-truncate univer-text-xs">
<span className="univer-text-red-500">{item.name.substring(0, searchText.length)}</span>
<span>{item.name.slice(searchText.length)}</span>
</span>
<span
className="univer-block univer-text-xs univer-text-gray-400"
>
{item.desc}
</span>
</li>
))}
</div>
</ul>
</RectPopup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -50,6 +50,13 @@ export function SelectFunction(props: ISelectFunctionProps) {
const sidebarService = useDependency(ISidebarService);
const sidebarOptions = useObservable<ISidebarMethodOptions>(sidebarService.sidebarOptions$);

const containerRef = useRef<HTMLUListElement>(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
Expand Down Expand Up @@ -172,38 +179,42 @@ export function SelectFunction(props: ISelectFunctionProps) {

{selectList.length > 0 && (
<ul
ref={containerRef}
{...containerProps}
className={clsx(`
univer-mb-0 univer-mt-2 univer-box-border univer-max-h-72 univer-w-full univer-select-none
univer-list-none univer-overflow-y-auto univer-rounded univer-p-3 univer-outline-none
`, borderClassName, scrollbarClassName)}
onKeyDown={handleSelectListKeyDown}
tabIndex={-1}
>
{selectList.map(({ name }, index) => (
<li
key={index}
className={clsx(`
univer-relative univer-box-border univer-cursor-pointer univer-rounded univer-px-7
univer-py-1 univer-text-sm univer-text-gray-900 univer-transition-colors
dark:!univer-text-white
`, {
'univer-bg-gray-200 dark:!univer-bg-gray-600': active === index,
})}
onMouseEnter={() => handleLiMouseEnter(index)}
onMouseLeave={handleLiMouseLeave}
onClick={() => setCurrentFunctionInfo(index)}
>
{nameSelected === index && (
<CheckMarkIcon
className={`
univer-absolute univer-left-1.5 univer-top-1/2 univer-inline-flex
-univer-translate-y-1/2 univer-text-base univer-text-primary-600
`}
/>
)}
<span className="univer-block">{highlightSearchText(name)}</span>
</li>
))}
<div style={wrapperStyle}>
{virtualList.map(({ data: { name }, index }) => (
<li
key={name}
className={clsx(`
univer-relative univer-box-border univer-cursor-pointer univer-rounded univer-px-7
univer-py-1 univer-text-sm univer-text-gray-900 univer-transition-colors
dark:!univer-text-white
`, {
'univer-bg-gray-200 dark:!univer-bg-gray-600': active === index,
})}
onMouseEnter={() => handleLiMouseEnter(index)}
onMouseLeave={handleLiMouseLeave}
onClick={() => setCurrentFunctionInfo(index)}
>
{nameSelected === index && (
<CheckMarkIcon
className={`
univer-absolute univer-left-1.5 univer-top-1/2 univer-inline-flex
-univer-translate-y-1/2 univer-text-base univer-text-primary-600
`}
/>
)}
<span className="univer-block">{highlightSearchText(name)}</span>
</li>
))}
</div>
</ul>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -87,4 +88,4 @@ export function SheetBarItem(props: IBaseSheetBarProps) {
</div>
</div>
);
}
});
3 changes: 2 additions & 1 deletion packages/ui/src/components/hooks/virtual-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,7 +24,7 @@ type ItemHeight<T> = (index: number, data: T) => number;
const isNumber = (value: unknown): value is number => typeof value === 'number';

export interface IVirtualListOptions<T> {
containerTarget: React.RefObject<HTMLElement>;
containerTarget: RefObject<HTMLElement | null>;
itemHeight: number | ItemHeight<T>;
overscan?: number;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<boolean>(menuItem.disabled$, false);
Expand Down Expand Up @@ -302,6 +303,13 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) {
return Array.isArray(selectorItem.selections) ? selectorItem.selections : [];
}, [menuItem.type, selectionsFromObservable, selectorItem.selections]);

const selectionsContainerRef = useRef<HTMLDivElement>(null);
const [virtualList, { wrapperStyle, containerProps }] = useVirtualList(selections, {
containerTarget: selectionsContainerRef,
itemHeight: 36,
overscan: 5,
});

const subMenuItems = useMemo(() => {
if (menuItem.type !== MenuItemType.SUBITEMS || !menuItem.id) {
return [];
Expand Down Expand Up @@ -513,6 +521,8 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) {
onWheel={(event) => event.stopPropagation()}
>
<div
ref={hasSelectionSubmenu ? selectionsContainerRef : undefined}
{...(hasSelectionSubmenu ? containerProps : {})}
className={clsx(
`
univer-overflow-y-auto univer-overscroll-contain univer-rounded-md univer-border
Expand All @@ -527,8 +537,8 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) {
}}
>
{hasSelectionSubmenu && (
<div className="univer-grid univer-gap-1">
{selections.map((option, index) => {
<div className="univer-grid univer-gap-1" style={wrapperStyle}>
{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);
Expand Down Expand Up @@ -631,7 +641,7 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) {
)}
</div>
);
}
});

function useContextGroupHiddenStates(menuSchemas: IMenuSchema[]) {
const [hiddenStates, setHiddenStates] = useState<Record<string, boolean>>({});
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/views/components/ribbon/ToolbarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,7 +30,7 @@ import { useToolbarItemStatus } from './hook';
import { ToolbarButton } from './ToolbarButton';
import { DropdownMenuWrapper, TooltipWrapper } from './TooltipButtonWrapper';

export const ToolbarItem = forwardRef<ITooltipWrapperRef, IDisplayMenuItem<IMenuItem>>((props, ref) => {
export const ToolbarItem = memo(forwardRef<ITooltipWrapperRef, IDisplayMenuItem<IMenuItem>>((props, ref) => {
const localeService = useDependency(LocaleService);
const commandService = useDependency(ICommandService);
const layoutService = useDependency(ILayoutService);
Expand Down Expand Up @@ -260,4 +260,4 @@ export const ToolbarItem = forwardRef<ITooltipWrapperRef, IDisplayMenuItem<IMenu
{renderItem()}
</TooltipWrapper>
);
});
}));
Loading
Loading