mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
feat(component): keyboard navigation for image-viewer (#2334)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -35,6 +35,20 @@ export const imagePreviewModalCloseButtonStyle = style({
|
|||||||
transition: 'background 0.2s ease-in-out',
|
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({
|
export const imagePreviewModalContainerStyle = style({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '20%',
|
top: '20%',
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { assertExists } from '@blocksuite/global/utils';
|
|||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
|
import { Suspense, useCallback } from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
imagePreviewModalCloseButtonStyle,
|
imagePreviewModalCloseButtonStyle,
|
||||||
imagePreviewModalContainerStyle,
|
imagePreviewModalContainerStyle,
|
||||||
|
imagePreviewModalGoStyle,
|
||||||
imagePreviewModalImageStyle,
|
imagePreviewModalImageStyle,
|
||||||
imagePreviewModalStyle,
|
imagePreviewModalStyle,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
@@ -28,6 +30,7 @@ const ImagePreviewModalImpl = (
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
|
const [blockId, setBlockId] = useAtom(previewBlockIdAtom);
|
||||||
const [caption, setCaption] = useState(() => {
|
const [caption, setCaption] = useState(() => {
|
||||||
const page = props.workspace.getPage(props.pageId);
|
const page = props.workspace.getPage(props.pageId);
|
||||||
assertExists(page);
|
assertExists(page);
|
||||||
@@ -96,14 +99,67 @@ const ImagePreviewModalImpl = (
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<span
|
||||||
|
className={imagePreviewModalGoStyle}
|
||||||
|
style={{
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
❮
|
||||||
|
</span>
|
||||||
<div className={imagePreviewModalContainerStyle}>
|
<div className={imagePreviewModalContainerStyle}>
|
||||||
<img
|
<img
|
||||||
|
data-blob-id={props.blockId}
|
||||||
alt={caption}
|
alt={caption}
|
||||||
className={imagePreviewModalImageStyle}
|
className={imagePreviewModalImageStyle}
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
src={url}
|
src={url}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
className={imagePreviewModalGoStyle}
|
||||||
|
style={{
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
❯
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -112,15 +168,74 @@ export const ImagePreviewModal = (
|
|||||||
props: ImagePreviewModalProps
|
props: ImagePreviewModalProps
|
||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
const [blockId, setBlockId] = useAtom(previewBlockIdAtom);
|
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) {
|
if (!blockId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImagePreviewModalImpl
|
<Suspense fallback={<div className={imagePreviewModalStyle} />}>
|
||||||
{...props}
|
<ImagePreviewModalImpl
|
||||||
blockId={blockId}
|
{...props}
|
||||||
onClose={() => setBlockId(null)}
|
blockId={blockId}
|
||||||
/>
|
onClose={() => setBlockId(null)}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { openHomePage } from '../libs/load-page';
|
import { openHomePage } from '../libs/load-page';
|
||||||
@@ -7,6 +8,38 @@ import {
|
|||||||
waitMarkdownImported,
|
waitMarkdownImported,
|
||||||
} from '../libs/page-logic';
|
} from '../libs/page-logic';
|
||||||
|
|
||||||
|
async function importImage(page: Page, url: string) {
|
||||||
|
await page.evaluate(
|
||||||
|
([url]) => {
|
||||||
|
const clipData = {
|
||||||
|
'text/html': `<img src=${url} />`,
|
||||||
|
};
|
||||||
|
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 }) => {
|
test('image preview should be shown', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
@@ -14,31 +47,57 @@ test('image preview should be shown', async ({ page }) => {
|
|||||||
const title = await getBlockSuiteEditorTitle(page);
|
const title = await getBlockSuiteEditorTitle(page);
|
||||||
await title.click();
|
await title.click();
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await page.evaluate(() => {
|
await importImage(page, 'http://localhost:8081/large-image.png');
|
||||||
const clipData = {
|
|
||||||
'text/html': `<img src="http://localhost:8081/large-image.png" />`,
|
|
||||||
};
|
|
||||||
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 page.locator('img').first().dblclick();
|
await page.locator('img').first().dblclick();
|
||||||
const locator = page.getByTestId('image-preview-modal');
|
const locator = page.getByTestId('image-preview-modal');
|
||||||
expect(locator.isVisible()).toBeTruthy();
|
expect(locator.isVisible()).toBeTruthy();
|
||||||
await page
|
await closeImagePreviewModal(page);
|
||||||
.getByTestId('image-preview-modal')
|
|
||||||
.locator('button')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(await locator.isVisible()).toBeFalsy();
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user