fix: rewrite selection logic and frame selection handling logic (#12421)

Fixes [BS-3528](https://linear.app/affine-design/issue/BS-3528)
Fixes [BS-3331](https://linear.app/affine-design/issue/BS-3331/frame-移动逻辑很奇怪)

### Changed
- Remove `onSelected` method from gfx view, use `handleSelection` provided by `GfxViewInteraction` instead.
- Add `selectable` to allow model to filter out itself from selection.
- Frame can be selected by body only if it's locked or its background is not transparent.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced selection behavior for frames, edgeless text, notes, and mind map elements with refined control based on lock state and background transparency.
  - Introduced group-aware selection logic promoting selection of appropriate group ancestors.
  - Added support for element selection events in interactivity extensions.

- **Bug Fixes**
  - Resolved frame selection issues by enabling selection via title clicks and restricting body selection to locked frames or those with non-transparent backgrounds.

- **Documentation**
  - Added clarifying comments for group retrieval methods.

- **Tests**
  - Updated and added end-to-end tests for frame and lock selection reflecting new selection conditions.

- **Refactor**
  - Unified and simplified selection handling by moving logic from component methods to interaction handlers and removing deprecated selection methods.
  - Streamlined selection candidate processing with extension-driven target suggestion.
  - Removed legacy group element retrieval and selection helper methods to simplify interaction logic.

- **Style**
  - Renamed types and improved type signatures for selection context and interaction configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
doouding
2025-05-26 05:03:08 +00:00
parent 14a89c1e8a
commit 5de63c29f5
21 changed files with 535 additions and 369 deletions

View File

@@ -16,8 +16,6 @@ import {
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
type,
@@ -57,21 +55,6 @@ test.describe('frame selection', () => {
expect(await getSelectedBoundCount(page)).toBe(0);
});
test('frame can selected by click blank area of frame if it has not title', async ({
page,
}) => {
await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await page.locator('affine-frame-title').dblclick();
await pressBackspace(page);
await pressEnter(page);
await clickView(page, [100, 100]);
expect(await getSelectedBoundCount(page)).toBe(1);
});
test('frame can be selected by click frame title', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
@@ -84,6 +67,59 @@ test.describe('frame selection', () => {
await assertSelectedBound(page, [50, 50, 100, 100]);
});
test('frame can be selected by body only if frame is locked or its background is not transparent', async ({
page,
}) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
const frameTitle = getFrameTitle(page, frame);
await frameTitle.click();
const getButtons = (page: Page) => {
const toolbar = page.locator('affine-toolbar-widget');
return {
lock: toolbar.getByTestId('lock'),
unlock: toolbar.getByTestId('unlock'),
};
};
const { lock, unlock } = await getButtons(page);
await lock.click();
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
// click body should select
await clickView(page, [100, 100]);
expect(await getSelectedBoundCount(page)).toBe(1);
await unlock.click();
// set frame background to non-transparent value
await page.evaluate(() => {
const edgelessRoot = document.querySelector('affine-edgeless-root');
if (!edgelessRoot) {
throw new Error('Not edgeless root found');
}
const gfx = edgelessRoot.gfx;
gfx.std.store.transact(() => {
// @ts-expect-error set frame block background
gfx.selection.selectedElements[0].props.background = '#fb7081';
});
});
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
// click body should select
await clickView(page, [100, 100]);
expect(await getSelectedBoundCount(page)).toBe(1);
});
test('frame can be selected by click frame title when a note overlap on it', async ({
page,
}) => {
@@ -112,8 +148,10 @@ test.describe('frame selection', () => {
expect(selectedIds[0]).toBe(frame);
});
test('shape inside frame can be selected and edited', async ({ page }) => {
await createFrame(page, [50, 50], [150, 150]);
test('shape inside frame can be directly selected and edited', async ({
page,
}) => {
const frame = await createFrame(page, [50, 50], [250, 250]);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await pressEscape(page);
@@ -121,6 +159,30 @@ test.describe('frame selection', () => {
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [100, 100, 100, 100]);
// select frame to change its background
const frameTitle = getFrameTitle(page, frame);
await frameTitle.click();
// set frame background to non-transparent value
await page.evaluate(() => {
const edgelessRoot = document.querySelector('affine-edgeless-root');
if (!edgelessRoot) {
throw new Error('Not edgeless root found');
}
const gfx = edgelessRoot.gfx;
gfx.std.store.transact(() => {
// @ts-expect-error set frame block background
gfx.selection.selectedElements[0].props.background = '#fb7081';
});
});
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await clickView(page, [150, 150]);
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [100, 100, 100, 100]);
await dblclickView(page, [150, 150]);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');

View File

@@ -194,7 +194,7 @@ test.describe('lock', () => {
await lock.click();
await assertEdgelessSelectedModelRect(page, [0, 0, 125, 125]); // frame outline and shape
await pressEscape(page);
await clickView(page, [100, 100]);
await clickView(page, [90, 90]);
await assertEdgelessSelectedModelRect(page, [0, 0, 125, 125]);
await unlock.click();