diff --git a/packages/component/src/components/image-preview-modal/index.css.ts b/packages/component/src/components/image-preview-modal/index.css.ts index da79fc3be2..1856d112b9 100644 --- a/packages/component/src/components/image-preview-modal/index.css.ts +++ b/packages/component/src/components/image-preview-modal/index.css.ts @@ -35,6 +35,20 @@ export const imagePreviewModalCloseButtonStyle = style({ transition: 'background 0.2s ease-in-out', }); +export const imagePreviewModalGoStyle = style({ + height: '50%', + color: 'var(--affine-white)', + position: 'absolute', + fontSize: '60px', + lineHeight: '60px', + fontWeight: 'bold', + display: 'flex', + alignItems: 'center', + opacity: '0.2', + padding: '0 15px', + cursor: 'pointer', +}); + export const imagePreviewModalContainerStyle = style({ position: 'absolute', top: '20%', diff --git a/packages/component/src/components/image-preview-modal/index.tsx b/packages/component/src/components/image-preview-modal/index.tsx index 97dc34ffa2..f1aa0f33b0 100644 --- a/packages/component/src/components/image-preview-modal/index.tsx +++ b/packages/component/src/components/image-preview-modal/index.tsx @@ -6,12 +6,14 @@ import { assertExists } from '@blocksuite/global/utils'; import type { Workspace } from '@blocksuite/store'; import { useAtom } from 'jotai'; import type { ReactElement } from 'react'; +import { Suspense, useCallback } from 'react'; import { useEffect, useRef, useState } from 'react'; import useSWR from 'swr'; import { imagePreviewModalCloseButtonStyle, imagePreviewModalContainerStyle, + imagePreviewModalGoStyle, imagePreviewModalImageStyle, imagePreviewModalStyle, } from './index.css'; @@ -28,6 +30,7 @@ const ImagePreviewModalImpl = ( onClose: () => void; } ): ReactElement | null => { + const [blockId, setBlockId] = useAtom(previewBlockIdAtom); const [caption, setCaption] = useState(() => { const page = props.workspace.getPage(props.pageId); assertExists(page); @@ -96,14 +99,67 @@ const ImagePreviewModalImpl = ( /> + { + assertExists(blockId); + const workspace = props.workspace; + + const page = workspace.getPage(props.pageId); + assertExists(page); + const block = page.getBlockById(blockId); + assertExists(block); + const prevBlock = page + .getPreviousSiblings(block) + .findLast( + (block): block is EmbedBlockModel => + block.flavour === 'affine:embed' + ); + if (prevBlock) { + setBlockId(prevBlock.id); + } + }} + > + ❮ +
{caption}
+ { + assertExists(blockId); + const workspace = props.workspace; + + const page = workspace.getPage(props.pageId); + assertExists(page); + const block = page.getBlockById(blockId); + assertExists(block); + const nextBlock = page + .getNextSiblings(block) + .find( + (block): block is EmbedBlockModel => + block.flavour === 'affine:embed' + ); + if (nextBlock) { + setBlockId(nextBlock.id); + } + }} + > + ❯ + ); }; @@ -112,15 +168,74 @@ export const ImagePreviewModal = ( props: ImagePreviewModalProps ): ReactElement | null => { const [blockId, setBlockId] = useAtom(previewBlockIdAtom); + + const handleKeyUp = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + event.stopPropagation(); + setBlockId(null); + return; + } + + if (!blockId) { + return; + } + + const workspace = props.workspace; + + const page = workspace.getPage(props.pageId); + assertExists(page); + const block = page.getBlockById(blockId); + assertExists(block); + + if (event.key === 'ArrowLeft') { + const prevBlock = page + .getPreviousSiblings(block) + .findLast( + (block): block is EmbedBlockModel => + block.flavour === 'affine:embed' + ); + if (prevBlock) { + setBlockId(prevBlock.id); + } + } else if (event.key === 'ArrowRight') { + const nextBlock = page + .getNextSiblings(block) + .find( + (block): block is EmbedBlockModel => + block.flavour === 'affine:embed' + ); + if (nextBlock) { + setBlockId(nextBlock.id); + } + } else { + return; + } + event.preventDefault(); + event.stopPropagation(); + }, + [blockId, setBlockId, props.workspace, props.pageId] + ); + + useEffect(() => { + document.addEventListener('keyup', handleKeyUp); + return () => { + document.removeEventListener('keyup', handleKeyUp); + }; + }, [handleKeyUp]); + if (!blockId) { return null; } return ( - setBlockId(null)} - /> + }> + setBlockId(null)} + /> + ); }; diff --git a/tests/fixtures/affine-preview.png b/tests/fixtures/affine-preview.png new file mode 100644 index 0000000000..46246f1952 Binary files /dev/null and b/tests/fixtures/affine-preview.png differ diff --git a/tests/parallels/image-preview.spec.ts b/tests/parallels/image-preview.spec.ts index b25efdf0ca..da7a046497 100644 --- a/tests/parallels/image-preview.spec.ts +++ b/tests/parallels/image-preview.spec.ts @@ -1,3 +1,4 @@ +import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { openHomePage } from '../libs/load-page'; @@ -7,6 +8,38 @@ import { waitMarkdownImported, } from '../libs/page-logic'; +async function importImage(page: Page, url: string) { + await page.evaluate( + ([url]) => { + const clipData = { + 'text/html': ``, + }; + const e = new ClipboardEvent('paste', { + clipboardData: new DataTransfer(), + }); + Object.defineProperty(e, 'target', { + writable: false, + value: document.body, + }); + Object.entries(clipData).forEach(([key, value]) => { + e.clipboardData?.setData(key, value); + }); + document.body.dispatchEvent(e); + }, + [url] + ); + await page.waitForTimeout(500); +} + +async function closeImagePreviewModal(page: Page) { + await page + .getByTestId('image-preview-modal') + .locator('button') + .first() + .click(); + await page.waitForTimeout(500); +} + test('image preview should be shown', async ({ page }) => { await openHomePage(page); await waitMarkdownImported(page); @@ -14,31 +47,57 @@ test('image preview should be shown', async ({ page }) => { const title = await getBlockSuiteEditorTitle(page); await title.click(); await page.keyboard.press('Enter'); - await page.evaluate(() => { - const clipData = { - 'text/html': ``, - }; - const e = new ClipboardEvent('paste', { - clipboardData: new DataTransfer(), - }); - Object.defineProperty(e, 'target', { - writable: false, - value: document.body, - }); - Object.entries(clipData).forEach(([key, value]) => { - e.clipboardData?.setData(key, value); - }); - document.body.dispatchEvent(e); - }); - await page.waitForTimeout(500); + await importImage(page, 'http://localhost:8081/large-image.png'); await page.locator('img').first().dblclick(); const locator = page.getByTestId('image-preview-modal'); expect(locator.isVisible()).toBeTruthy(); - await page - .getByTestId('image-preview-modal') - .locator('button') - .first() - .click(); - await page.waitForTimeout(500); + await closeImagePreviewModal(page); expect(await locator.isVisible()).toBeFalsy(); }); + +test('image go left and right', async ({ page }) => { + await openHomePage(page); + await waitMarkdownImported(page); + await newPage(page); + let blobId: string; + { + const title = await getBlockSuiteEditorTitle(page); + await title.click(); + await page.keyboard.press('Enter'); + await importImage(page, 'http://localhost:8081/large-image.png'); + await page.locator('img').first().dblclick(); + await page.waitForTimeout(500); + blobId = (await page + .locator('img') + .nth(1) + .getAttribute('data-blob-id')) as string; + expect(blobId).toBeTruthy(); + await closeImagePreviewModal(page); + } + { + const title = await getBlockSuiteEditorTitle(page); + await title.click(); + await page.keyboard.press('Enter'); + await importImage(page, 'http://localhost:8081/affine-preview.png'); + } + const locator = page.getByTestId('image-preview-modal'); + expect(locator.isVisible()).toBeTruthy(); + await page.locator('img').first().dblclick(); + await page.waitForTimeout(5000); + { + const newBlobId = (await page + .locator('img[data-blob-id]') + .first() + .getAttribute('data-blob-id')) as string; + expect(newBlobId).not.toBe(blobId); + } + await page.keyboard.press('ArrowRight'); + await page.waitForTimeout(5000); + { + const newBlobId = (await page + .locator('img[data-blob-id]') + .first() + .getAttribute('data-blob-id')) as string; + expect(newBlobId).toBe(blobId); + } +});