diff --git a/packages/web-app-preview/src/components/MediaControls.vue b/packages/web-app-preview/src/components/MediaControls.vue index 5f68ea337b6..1503d99c0d1 100644 --- a/packages/web-app-preview/src/components/MediaControls.vue +++ b/packages/web-app-preview/src/components/MediaControls.vue @@ -266,8 +266,13 @@ const resourceFavoriteIconDescription = computed(() => { const enterFullScreenDescription = computed(() => $gettext('Enter full screen mode')) const exitFullScreenDescription = computed(() => $gettext('Exit full screen mode')) -const imageShrinkDescription = computed(() => $gettext('Shrink the image (⇧ + Mouse wheel)')) -const imageZoomDescription = computed(() => $gettext('Enlarge the image (⇧ + Mouse wheel)')) +const zoomModifierKey = computed(() => (isMacOs() ? '⌘' : $gettext('Ctrl'))) +const imageShrinkDescription = computed(() => + $gettext('Shrink the image (%{key} + Mouse wheel)', { key: unref(zoomModifierKey) }) +) +const imageZoomDescription = computed(() => + $gettext('Enlarge the image (%{key} + Mouse wheel)', { key: unref(zoomModifierKey) }) +) const imageResetDescription = computed(() => $gettext('Reset')) const imageRotateLeftDescription = computed(() => $gettext('Rotate the image 90 degrees to the left') diff --git a/packages/web-app-preview/src/components/Sources/MediaImage.vue b/packages/web-app-preview/src/components/Sources/MediaImage.vue index 94f95ec2942..1df60d48ffd 100644 --- a/packages/web-app-preview/src/components/Sources/MediaImage.vue +++ b/packages/web-app-preview/src/components/Sources/MediaImage.vue @@ -76,7 +76,7 @@ function sanitizeSvgElement(svg: SVGElement) { const onWheelEvent = (e: WheelEvent) => { e.preventDefault() - if (!e.shiftKey) { + if (!e.shiftKey && !e.ctrlKey && !e.metaKey) { return false } diff --git a/packages/web-app-preview/tests/unit/components/MediaImage.spec.ts b/packages/web-app-preview/tests/unit/components/MediaImage.spec.ts index 755c2a001a9..a3f41b45df2 100644 --- a/packages/web-app-preview/tests/unit/components/MediaImage.spec.ts +++ b/packages/web-app-preview/tests/unit/components/MediaImage.spec.ts @@ -1,17 +1,19 @@ import MediaImage from '../../../src/components/Sources/MediaImage.vue' import { defaultPlugins, shallowMount } from '@opencloud-eu/web-test-helpers' +import { flushPromises } from '@vue/test-utils' import { Resource } from '@opencloud-eu/web-client' import { MediaFile } from '../../../src/helpers/types' // panzoom touches real DOM geometry which jsdom does not provide -vi.mock('@panzoom/panzoom', () => ({ - default: () => ({ - destroy: vi.fn(), - setStyle: vi.fn(), - getScale: () => 1, - getPan: () => ({ x: 0, y: 0 }) - }) -})) +const panzoom = { + destroy: vi.fn(), + setStyle: vi.fn(), + zoomIn: vi.fn(), + zoomOut: vi.fn(), + getScale: () => 1, + getPan: () => ({ x: 0, y: 0 }) +} +vi.mock('@panzoom/panzoom', () => ({ default: () => panzoom })) const mediaFile = (mimeType: string): MediaFile => ({ @@ -29,7 +31,20 @@ const mediaFile = (mimeType: string): MediaFile => resource: { id: 'img1' } as Resource }) as MediaFile +// the test DOM drops modifier keys passed to the WheelEvent constructor +function wheelEvent(deltaY: number, modifier: Partial = {}) { + const event = new WheelEvent('wheel', { deltaY }) + Object.entries(modifier).forEach(([key, value]) => { + Object.defineProperty(event, key, { value }) + }) + return event +} + describe('MediaImage component', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + it('fits an svg to the stage: stage-sized wrapper and a size-full svg', () => { // the wrapper must have a size so the svg can scale to it instead of // overflowing (large svg) or collapsing to 0x0 (viewBox-only svg) @@ -49,6 +64,33 @@ describe('MediaImage component', () => { const img = wrapper.find('img') expect(img.classes()).toEqual(expect.arrayContaining(['max-w-full', 'max-h-full'])) }) + + describe('mouse wheel zoom', () => { + it.each([ + ['shift', { shiftKey: true }], + ['ctrl', { ctrlKey: true }], + ['meta', { metaKey: true }] + ])('zooms with the %s key held', async (_, modifier) => { + const { wrapper } = getWrapper('image/png') + await flushPromises() + + wrapper.find('img').element.dispatchEvent(wheelEvent(-1, modifier)) + expect(panzoom.zoomIn).toHaveBeenCalledWith({ step: 0.1 }) + + wrapper.find('img').element.dispatchEvent(wheelEvent(1, modifier)) + expect(panzoom.zoomOut).toHaveBeenCalledWith({ step: 0.1 }) + }) + + it('does not zoom without a modifier key', async () => { + const { wrapper } = getWrapper('image/png') + await flushPromises() + + wrapper.find('img').element.dispatchEvent(wheelEvent(-1)) + + expect(panzoom.zoomIn).not.toHaveBeenCalled() + expect(panzoom.zoomOut).not.toHaveBeenCalled() + }) + }) }) function getWrapper(mimeType: string) {