mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-23 16:22:21 +03:00
feat(component): keyboard navigation for image-viewer (#2334)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
parent
259d7988d9
commit
48c109e149
@ -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%',
|
||||
|
@ -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 = (
|
||||
/>
|
||||
</svg>
|
||||
</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}>
|
||||
<img
|
||||
data-blob-id={props.blockId}
|
||||
alt={caption}
|
||||
className={imagePreviewModalImageStyle}
|
||||
ref={imageRef}
|
||||
src={url}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@ -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 (
|
||||
<ImagePreviewModalImpl
|
||||
{...props}
|
||||
blockId={blockId}
|
||||
onClose={() => setBlockId(null)}
|
||||
/>
|
||||
<Suspense fallback={<div className={imagePreviewModalStyle} />}>
|
||||
<ImagePreviewModalImpl
|
||||
{...props}
|
||||
blockId={blockId}
|
||||
onClose={() => setBlockId(null)}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
BIN
tests/fixtures/affine-preview.png
vendored
Normal file
BIN
tests/fixtures/affine-preview.png
vendored
Normal file
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 { 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': `<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 }) => {
|
||||
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': `<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 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);
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user