mirror of
https://github.com/dream-num/univer.git
synced 2026-10-02 04:24:34 +08:00
fix(ui): prevent browser zoom in floating menus (#7489)
This commit is contained in:
@@ -16,8 +16,8 @@
|
||||
|
||||
import type { IMenuManagerService as IMenuManagerServiceType, IMenuSchema } from '@univerjs/ui';
|
||||
import { borderClassName, clsx } from '@univerjs/design';
|
||||
import { IMenuManagerService, MenuManagerPosition, ToolbarItem, useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IMenuManagerService, MenuManagerPosition, preventBrowserZoomInContainers, ToolbarItem, useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
SetInlineFormatBoldCommand,
|
||||
SetInlineFormatFontSizeCommand,
|
||||
@@ -92,6 +92,7 @@ export function FloatToolbar(props: IFloatToolbarProps) {
|
||||
const { avaliableMenus = DEFAULT_AVALIABLE_MENUS } = props;
|
||||
|
||||
const menuManagerService = useDependency(IMenuManagerService);
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [menus, setMenus] = useState<IFloatToolbarMenuSchema[]>([]);
|
||||
const [extraMenus, setExtraMenus] = useState<IMenuSchema[]>([]);
|
||||
@@ -112,8 +113,18 @@ export function FloatToolbar(props: IFloatToolbarProps) {
|
||||
};
|
||||
}, [avaliableMenus, menuManagerService]);
|
||||
|
||||
useEffect(() => {
|
||||
const toolbar = toolbarRef.current;
|
||||
if (!toolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
return preventBrowserZoomInContainers([toolbar]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
className={clsx(`
|
||||
univer-box-border univer-flex univer-rounded univer-bg-white univer-py-1.5 univer-shadow-sm
|
||||
dark:!univer-border-gray-700 dark:!univer-bg-gray-900
|
||||
|
||||
@@ -181,6 +181,7 @@ export type { IFontFamilyItemProps } from './views/font-family/FontFamilyItem';
|
||||
export { FontSize } from './views/font-size/FontSize';
|
||||
export { FONT_SIZE_COMPONENT, FONT_SIZE_LIST, HEADING_LIST } from './views/font-size/interface';
|
||||
export * from './views/hooks/index';
|
||||
export { preventBrowserZoomInContainers } from './views/hooks/prevent-browser-zoom';
|
||||
export * from './views/index';
|
||||
export { type INotificationOptions } from './views/notification/Notification';
|
||||
export { ProgressBar } from './views/progress-bar/ProgressBar';
|
||||
|
||||
@@ -36,6 +36,7 @@ import { IMenuManagerService } from '../../../services/menu/menu-manager.service
|
||||
import { useDependency, useObservable } from '../../../utils/di';
|
||||
import { CustomLabel } from '../../custom-label/CustomLabel';
|
||||
import { useScrollYOverContainer } from '../../hooks/layout';
|
||||
import { preventBrowserZoomInContainers } from '../../hooks/prevent-browser-zoom';
|
||||
import { resolveMenuItemActiveState, UIQuickTileMenuGroup, UITinyMenuGroup } from '../../menu/desktop/TinyMenuGroup';
|
||||
|
||||
type ContextMenuSizeVariant = 'default' | 'paragraph-t';
|
||||
@@ -702,6 +703,14 @@ export function ContextMenuPanel(props: IContextMenuPanelProps) {
|
||||
|
||||
useScrollYOverContainer(menuElement, layoutService.rootContainerElement);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
return preventBrowserZoomInContainers([menuElement]);
|
||||
}, [menuElement]);
|
||||
|
||||
const getFocusableMenuButtons = useCallback(() => {
|
||||
if (!menuElement) {
|
||||
return [];
|
||||
@@ -1195,6 +1204,15 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) {
|
||||
}
|
||||
}, [closeSubmenu, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const submenuElement = submenuElementRef.current;
|
||||
if (!submenuVisible || !submenuElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
return preventBrowserZoomInContainers([submenuElement]);
|
||||
}, [submenuVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!submenuVisible) {
|
||||
setSubmenuPositionReady(false);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { preventBrowserZoomInContainers } from '../prevent-browser-zoom';
|
||||
|
||||
describe('preventBrowserZoomInContainers', () => {
|
||||
it('prevents modified wheel defaults before a child stops propagation', () => {
|
||||
const container = document.createElement('div');
|
||||
const child = document.createElement('div');
|
||||
const childListener = vi.fn((event: WheelEvent) => event.stopPropagation());
|
||||
container.appendChild(child);
|
||||
child.addEventListener('wheel', childListener);
|
||||
const dispose = preventBrowserZoomInContainers([container]);
|
||||
|
||||
const event = new WheelEvent('wheel', { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(event, 'ctrlKey', { value: true });
|
||||
child.dispatchEvent(event);
|
||||
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(childListener).toHaveBeenCalledTimes(1);
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('leaves ordinary wheel scrolling unchanged', () => {
|
||||
const container = document.createElement('div');
|
||||
const dispose = preventBrowserZoomInContainers([container]);
|
||||
const event = new WheelEvent('wheel', { bubbles: true, cancelable: true });
|
||||
|
||||
container.dispatchEvent(event);
|
||||
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('prevents Safari gesture zoom and removes every listener on cleanup', () => {
|
||||
const container = document.createElement('div');
|
||||
const dispose = preventBrowserZoomInContainers([container]);
|
||||
const gestureStart = new Event('gesturestart', { bubbles: true, cancelable: true });
|
||||
const gestureChange = new Event('gesturechange', { bubbles: true, cancelable: true });
|
||||
|
||||
container.dispatchEvent(gestureStart);
|
||||
container.dispatchEvent(gestureChange);
|
||||
|
||||
expect(gestureStart.defaultPrevented).toBe(true);
|
||||
expect(gestureChange.defaultPrevented).toBe(true);
|
||||
|
||||
dispose();
|
||||
const wheelAfterDispose = new WheelEvent('wheel', { bubbles: true, cancelable: true, metaKey: true });
|
||||
const gestureAfterDispose = new Event('gesturestart', { bubbles: true, cancelable: true });
|
||||
container.dispatchEvent(wheelAfterDispose);
|
||||
container.dispatchEvent(gestureAfterDispose);
|
||||
|
||||
expect(wheelAfterDispose.defaultPrevented).toBe(false);
|
||||
expect(gestureAfterDispose.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function preventBrowserZoomInContainers(containers: readonly HTMLElement[]): () => void {
|
||||
const uniqueContainers = new Set(containers);
|
||||
const handleWheel = (event: WheelEvent): void => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
const handleGesture = (event: Event): void => event.preventDefault();
|
||||
|
||||
uniqueContainers.forEach((container) => {
|
||||
container.addEventListener('wheel', handleWheel, { capture: true, passive: false });
|
||||
container.addEventListener('gesturestart', handleGesture, { capture: true, passive: false });
|
||||
container.addEventListener('gesturechange', handleGesture, { capture: true, passive: false });
|
||||
});
|
||||
|
||||
return () => {
|
||||
uniqueContainers.forEach((container) => {
|
||||
container.removeEventListener('wheel', handleWheel, true);
|
||||
container.removeEventListener('gesturestart', handleGesture, true);
|
||||
container.removeEventListener('gesturechange', handleGesture, true);
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user