perf: (design): enhance menu change handling and improve fake toolbar rendering (#5219)

This commit is contained in:
白熱
2025-05-17 14:43:10 +08:00
committed by GitHub
parent 65f8bf37ad
commit 3547b69810
5 changed files with 103 additions and 83 deletions
+2
View File
@@ -22,6 +22,7 @@ import { UniverSheetsDataValidationUIPlugin } from '@univerjs/sheets-data-valida
import { UniverSheetsDrawingUIPlugin } from '@univerjs/sheets-drawing-ui';
import { UniverSheetsFilterUIPlugin } from '@univerjs/sheets-filter-ui';
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
import { UniverSheetsNoteUIPlugin } from '@univerjs/sheets-note-ui';
import { UniverSheetsNumfmtUIPlugin } from '@univerjs/sheets-numfmt-ui';
import { UniverSheetsTableUIPlugin } from '@univerjs/sheets-table-ui';
import { UniverSheetsThreadCommentUIPlugin } from '@univerjs/sheets-thread-comment-ui';
@@ -34,6 +35,7 @@ export default function getLazyPlugins(): Array<[PluginCtor<Plugin>] | [PluginCt
[UniverSheetsNumfmtUIPlugin],
[UniverThreadCommentUIPlugin],
[UniverSheetsThreadCommentUIPlugin],
[UniverSheetsNoteUIPlugin],
[UniverSheetsTableUIPlugin],
[UniverSheetsFormulaUIPlugin],
[UniverSheetsDataValidationUIPlugin],
-2
View File
@@ -31,7 +31,6 @@ import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
import { UniverSheetsNotePlugin } from '@univerjs/sheets-note';
import { UniverSheetsNoteUIPlugin } from '@univerjs/sheets-note-ui';
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort';
import { UniverSheetsTablePlugin } from '@univerjs/sheets-table';
@@ -132,7 +131,6 @@ function createNewInstance() {
univer.registerPlugin(UniverSheetsTablePlugin);
univer.registerPlugin(UniverNetworkPlugin);
univer.registerPlugin(UniverSheetsNotePlugin);
univer.registerPlugin(UniverSheetsNoteUIPlugin);
univer.registerPlugin(ImportCSVButtonPlugin);
// If we are running in e2e platform, we should immediately register the debugger plugin.
@@ -72,13 +72,13 @@ export function UniToolbar() {
return (
<div
className={`
univer-flex univer-p-2 univer-items-center univer-gap-2 univer-rounded-lg univer-border
univer-border-gray-300 univer-overflow-hidden univer-shadow-lg univer-select-none univer-relative
univer-box-border univer-text-sm univer-bg-white
univer-relative univer-box-border univer-flex univer-select-none univer-items-center univer-gap-2
univer-overflow-hidden univer-rounded-lg univer-border univer-border-gray-300 univer-bg-white univer-p-2
univer-text-sm univer-shadow-lg
`}
>
{hasWorkbooks && <UniFormulaBar />}
<div className="univer-flex univer-gap-1 univer-items-center univer-shrink-0">
<div className="univer-flex univer-shrink-0 univer-items-center univer-gap-1">
{uniVisibleItems.map((subItem) => subItem && <ToolbarItem key={subItem.id} {...subItem} />)}
</div>
</div>
@@ -34,7 +34,7 @@ export interface IMenuSchema {
}
export interface IMenuManagerService {
readonly menuChanged$: Observable<number>;
readonly menuChanged$: Observable<void>;
mergeMenu(source: MenuSchemaType, target?: MenuSchemaType): void;
@@ -54,7 +54,7 @@ export type MenuSchemaType = {
};
export class MenuManagerService extends Disposable implements IMenuManagerService {
readonly menuChanged$ = new Subject<number>();
readonly menuChanged$ = new Subject<void>();
private _menu: MenuSchemaType = {
[MenuManagerPosition.RIBBON]: {
@@ -243,7 +243,7 @@ export class MenuManagerService extends Disposable implements IMenuManagerServic
const _key = key as keyof MenuSchemaType;
_target[_key] = merge({}, _target[_key], source[_key]);
this.menuChanged$.next(Date.now());
this.menuChanged$.next();
} else if (typeof value === 'object') {
this.mergeMenu(source, value);
}
@@ -252,7 +252,7 @@ export class MenuManagerService extends Disposable implements IMenuManagerServic
appendRootMenu(source: MenuSchemaType): void {
this._menu = merge({}, this._menu, source);
this.menuChanged$.next(Date.now());
this.menuChanged$.next();
}
private _buildMenuSchema(data: MenuSchemaType): IMenuSchema[] {
@@ -17,14 +17,14 @@
import type { ComponentType } from 'react';
import type { Observable } from 'rxjs';
import type { IMenuSchema } from '../../../services/menu/menu-manager.service';
import { LocaleService } from '@univerjs/core';
import { LocaleService, throttle } from '@univerjs/core';
import { borderBottomClassName, borderClassName, clsx, divideXClassName, Dropdown, HoverCard } from '@univerjs/design';
import { DatabaseSingle, EyeSingle, FunctionSingle, HomeSingle, InsertSingle, MoreDownSingle, MoreFunctionSingle } from '@univerjs/icons';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { combineLatest } from 'rxjs';
import { IMenuManagerService } from '../../../services/menu/menu-manager.service';
import { MenuManagerPosition, RibbonPosition } from '../../../services/menu/types';
import { useDependency, useObservable } from '../../../utils/di';
import { useDependency } from '../../../utils/di';
import { ComponentContainer } from '../ComponentContainer';
import { ToolbarButton } from './ToolbarButton';
import { ToolbarItem } from './ToolbarItem';
@@ -48,11 +48,19 @@ export function Ribbon(props: IRibbonProps) {
const menuManagerService = useDependency(IMenuManagerService);
const localeService = useDependency(LocaleService);
const [menuChangedTimes, setMenuChangedTimes] = useState(0);
const menuChanged = useObservable(menuManagerService.menuChanged$);
useEffect(() => {
const subscription = menuManagerService.menuChanged$.subscribe(() => {
setMenuChangedTimes((prev) => prev + 1);
});
return () => {
subscription.unsubscribe();
};
}, []);
const containerRef = useRef<HTMLDivElement>(null!);
const fakeToolbarRef = useRef<HTMLDivElement>(null!);
const toolbarItemRefs = useRef<Record<string, {
el: HTMLSpanElement;
key: string;
@@ -64,6 +72,7 @@ export function Ribbon(props: IRibbonProps) {
const [activatedTab, setActivatedTab] = useState<string>(RibbonPosition.START);
const [groupSelectorVisible, setGroupSelectorVisible] = useState(false);
const [collapsedIds, setCollapsedIds] = useState<string[]>([]);
const [fakeToolbarVisible, setFakeToolbarVisible] = useState(false);
// const [changingActiveTab, setChangingActiveTab] = useState(false);
const handleSelectTab = useCallback((group: IMenuSchema) => {
@@ -80,10 +89,11 @@ export function Ribbon(props: IRibbonProps) {
};
}, []);
// subscribe to menu changes
// process menu changes
useEffect(() => {
const ribbon = menuManagerService.getMenuByPositionKey(MenuManagerPosition.RIBBON);
// Collect all hidden$ Observables and their corresponding paths
const hiddenObservaleMap: Observable<boolean>[] = [];
const hiddenKeyMap: string[] = [];
for (const group of ribbon) {
@@ -101,8 +111,8 @@ export function Ribbon(props: IRibbonProps) {
}
}
const subscription = combineLatest(hiddenObservaleMap)
// .pipe(debounceTime(300))
// Only get the current value once, not continuously subscribe
combineLatest(hiddenObservaleMap)
.subscribe((hiddenMap) => {
const newRibbon: IMenuSchema[] = [];
@@ -147,12 +157,9 @@ export function Ribbon(props: IRibbonProps) {
}
setRibbon(newRibbon);
});
return () => {
subscription.unsubscribe();
};
}, [menuChanged]);
})
.unsubscribe();
}, [menuChangedTimes]);
const activeGroup = useMemo(() => {
const allGroups = ribbon.find((group) => group.key === activatedTab)?.children ?? [];
@@ -186,37 +193,43 @@ export function Ribbon(props: IRibbonProps) {
}, [collapsedIds, ribbon, activatedTab]);
useEffect(() => {
const observer = new ResizeObserver((entries) => {
const observer = new ResizeObserver(throttle((entries) => {
for (const entry of entries) {
const { width: avaliableWidth } = entry.contentRect;
const toolbarItems = Object.values(toolbarItemRefs.current);
const sortedToolbarItems = toolbarItems.sort((a, b) => {
if (a.groupOrder === b.groupOrder) {
return a.order - b.order;
setFakeToolbarVisible(true);
requestAnimationFrame(() => {
const { width: avaliableWidth } = entry.contentRect;
const toolbarItems = Object.values(toolbarItemRefs.current);
const sortedToolbarItems = toolbarItems.sort((a, b) => {
if (a.groupOrder === b.groupOrder) {
return a.order - b.order;
}
return a.groupOrder - b.groupOrder;
});
const newCollapsedIds: string[] = [];
let totalWidth = 32;
const allGroups = ribbon.find((group) => group.key === activatedTab)?.children ?? [];
const gapWidth = (allGroups.length - 1) * 8;
totalWidth += gapWidth;
for (const { el, key } of sortedToolbarItems) {
if (!el) continue;
totalWidth += el?.getBoundingClientRect().width + 8;
if (totalWidth > avaliableWidth - gapWidth) {
newCollapsedIds.push(key);
}
}
return a.groupOrder - b.groupOrder;
setCollapsedIds(newCollapsedIds);
setFakeToolbarVisible(false);
});
const newCollapsedIds: string[] = [];
let totalWidth = 32;
const allGroups = ribbon.find((group) => group.key === activatedTab)?.children ?? [];
const gapWidth = (allGroups.length - 1) * 8;
totalWidth += gapWidth;
for (const { el, key } of sortedToolbarItems) {
if (!el) continue;
totalWidth += el?.getBoundingClientRect().width + 8;
if (totalWidth > avaliableWidth - gapWidth) {
newCollapsedIds.push(key);
}
}
setCollapsedIds(newCollapsedIds);
}
});
}, 300));
observer.observe(containerRef.current);
@@ -225,6 +238,45 @@ export function Ribbon(props: IRibbonProps) {
};
}, [ribbon, activatedTab]);
const fakeToolbar = useMemo(() => {
if (!fakeToolbarVisible) return null;
return (
<div
aria-hidden="true"
className={clsx(`
univer-invisible univer-absolute -univer-left-[99999] -univer-top-[99999] univer-box-border
univer-flex univer-h-10 univer-min-w-min univer-items-center univer-px-3 univer-opacity-0
`, borderBottomClassName)}
>
{activeGroup.allGroups.map((groupItem) => (groupItem.children?.length || groupItem.item) && (
<Fragment key={groupItem.key}>
<div className="univer-grid univer-grid-flow-col univer-gap-2 univer-px-2">
{groupItem.children && groupItem.children?.map((child) => (
child.item && (
<ToolbarItem
key={child.key}
{...child.item}
ref={(ref) => {
if (ref?.el) {
toolbarItemRefs.current[child.key] = {
el: ref.el,
key: child.key,
groupOrder: groupItem.order,
order: child.order,
};
}
}}
/>
)
))}
</div>
</Fragment>
))}
</div>
);
}, [activeGroup.allGroups, fakeToolbarVisible]);
return (
<>
{headerMenu && (headerMenuComponents && headerMenuComponents.size > 0) && (
@@ -377,39 +429,7 @@ export function Ribbon(props: IRibbonProps) {
</div>
{/* fake toolbar */}
<div
ref={fakeToolbarRef}
aria-hidden
className={clsx(`
univer-invisible univer-absolute -univer-left-[99999] -univer-top-[99999] univer-box-border
univer-flex univer-h-10 univer-min-w-min univer-items-center univer-px-3 univer-opacity-0
`, borderBottomClassName)}
>
{activeGroup.allGroups.map((groupItem) => (groupItem.children?.length || groupItem.item) && (
<Fragment key={groupItem.key}>
<div className="univer-grid univer-grid-flow-col univer-gap-2 univer-px-2">
{groupItem.children && groupItem.children?.map((child) => (
child.item && (
<ToolbarItem
key={child.key}
{...child.item}
ref={(ref) => {
if (ref?.el) {
toolbarItemRefs.current[child.key] = {
el: ref.el,
key: child.key,
groupOrder: groupItem.order,
order: child.order,
};
}
}}
/>
)
))}
</div>
</Fragment>
))}
</div>
{fakeToolbar}
</>
);
}