feat(editor): add shortcut to highlighter tool (#11604)

Closes: [BS-3092](https://linear.app/affine-design/issue/BS-3092/highlighter-快捷键)

### What's Changed!

* Added shortcut `⇧ P` to highlighter tool

[Screen Recording 2025-04-10 at 16.33.30.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/8ypiIKZXudF5a0tIgIzf/38aadc08-ed18-4b48-9d91-b4876d14a2d3.mov" />](https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/38aadc08-ed18-4b48-9d91-b4876d14a2d3.mov)
This commit is contained in:
fundon
2025-04-11 13:08:59 +00:00
parent aabb09b31f
commit afdc40b510
8 changed files with 177 additions and 38 deletions
@@ -88,6 +88,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
p: () => { p: () => {
this._setEdgelessTool('brush'); this._setEdgelessTool('brush');
}, },
'Shift-p': () => {
this._setEdgelessTool('highlighter');
},
e: () => { e: () => {
this._setEdgelessTool('eraser'); this._setEdgelessTool('eraser');
}, },
@@ -1,5 +1,6 @@
import { css, html, LitElement } from 'lit'; import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { repeat } from 'lit-html/directives/repeat.js';
export class TooltipContentWithShortcut extends LitElement { export class TooltipContentWithShortcut extends LitElement {
static override styles = css` static override styles = css`
@@ -9,6 +10,10 @@ export class TooltipContentWithShortcut extends LitElement {
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.tooltip__shortcuts {
display: flex;
gap: 2px;
}
.tooltip__shortcut { .tooltip__shortcut {
font-size: 12px; font-size: 12px;
position: relative; position: relative;
@@ -28,19 +33,30 @@ export class TooltipContentWithShortcut extends LitElement {
opacity: 0.2; opacity: 0.2;
} }
.tooltip__label { .tooltip__label {
display: flex;
flex: 1;
white-space: pre; white-space: pre;
} }
`; `;
get shortcuts() {
let shortcut = this.shortcut;
if (!shortcut) return [];
return shortcut.split(' ');
}
override render() { override render() {
const { tip, shortcut, postfix } = this; const { tip, shortcuts, postfix } = this;
return html` return html`
<div class="tooltip-with-shortcut"> <div class="tooltip-with-shortcut">
<span class="tooltip__label">${tip}</span> <span class="tooltip__label">${tip}</span>
${shortcut <div class="tooltip__shortcuts">
? html`<span class="tooltip__shortcut">${shortcut}</span>` ${repeat(
: ''} shortcuts,
shortcut => html`<span class="tooltip__shortcut">${shortcut}</span>`
)}
</div>
${postfix ? html`<span class="tooltip__postfix">${postfix}</span>` : ''} ${postfix ? html`<span class="tooltip__postfix">${postfix}</span>` : ''}
</div> </div>
`; `;
@@ -0,0 +1,29 @@
import {
EdgelessBrushDarkIcon,
EdgelessBrushLightIcon,
EdgelessHighlighterDarkIcon,
EdgelessHighlighterLightIcon,
} from './icons';
import type { Pen } from './types';
export const penIconMap = {
dark: {
brush: EdgelessBrushDarkIcon,
highlighter: EdgelessHighlighterDarkIcon,
},
light: {
brush: EdgelessBrushLightIcon,
highlighter: EdgelessHighlighterLightIcon,
},
};
export const penInfoMap: { [k in Pen]: { tip: string; shortcut: string } } = {
brush: {
tip: 'Pen',
shortcut: 'P',
},
highlighter: {
tip: 'Highlighter',
shortcut: '⇧ P',
},
};
@@ -7,11 +7,16 @@ import {
import type { ColorEvent } from '@blocksuite/affine-shared/utils'; import type { ColorEvent } from '@blocksuite/affine-shared/utils';
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar'; import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import { SignalWatcher } from '@blocksuite/global/lit'; import { SignalWatcher } from '@blocksuite/global/lit';
import { computed, type Signal } from '@preact/signals-core'; import {
computed,
type ReadonlySignal,
type Signal,
} from '@preact/signals-core';
import { css, html, LitElement, type TemplateResult } from 'lit'; import { css, html, LitElement, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { penInfoMap } from './consts';
import type { Pen, PenMap } from './types'; import type { Pen, PenMap } from './types';
export class EdgelessPenMenu extends EdgelessToolbarToolMixin( export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
@@ -26,8 +31,14 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
.pens { .pens {
display: flex; display: flex;
height: 100%;
padding: 0 4px; padding: 0 4px;
align-items: center; align-items: flex-end;
edgeless-tool-icon-button {
display: flex;
align-self: flex-start;
}
.pen-wrapper { .pen-wrapper {
display: flex; display: flex;
@@ -36,7 +47,7 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
position: relative; position: relative;
transform: translateY(10px); transform: translateY(-2px);
transition-property: color, transform; transition-property: color, transform;
transition-duration: 300ms; transition-duration: 300ms;
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
@@ -46,7 +57,7 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
.pen-wrapper:hover, .pen-wrapper:hover,
.pen-wrapper:active, .pen-wrapper:active,
.pen-wrapper[data-active] { .pen-wrapper[data-active] {
transform: translateY(-10px); transform: translateY(-22px);
} }
} }
@@ -56,6 +67,8 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
} }
menu-divider { menu-divider {
display: flex;
align-self: center;
height: 24px; height: 24px;
margin: 0 9px; margin: 0 9px;
} }
@@ -83,42 +96,64 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
override render() { override render() {
const { const {
_theme$: { value: theme }, _theme$: { value: theme },
color$: { value: currentColor },
colors$: { colors$: {
value: { brush: brushColor, highlighter: highlighterColor }, value: { brush: brushColor, highlighter: highlighterColor },
}, },
pen$: { value: pen },
penIconMap$: { penIconMap$: {
value: { brush: brushIcon, highlighter: highlighterIcon }, value: { brush: brushIcon, highlighter: highlighterIcon },
}, },
penInfo$: {
value: { type, color },
},
} = this; } = this;
return html` return html`
<edgeless-slide-menu> <edgeless-slide-menu>
<div class="pens" slot="prefix"> <div class="pens" slot="prefix">
<div <edgeless-tool-icon-button
class="pen-wrapper edgeless-brush-button" class="edgeless-brush-button"
?data-active="${pen === 'brush'}" .tooltip=${html`<affine-tooltip-content-with-shortcut
style=${styleMap({ color: brushColor })} data-tip="${penInfoMap.brush.tip}"
data-shortcut="${penInfoMap.brush.shortcut}"
></affine-tooltip-content-with-shortcut>`}
.tooltipOffset=${20}
.hover=${false}
@click=${() => this._onPickPen('brush')} @click=${() => this._onPickPen('brush')}
> >
${brushIcon} <div
</div> class="pen-wrapper"
<div style=${styleMap({ color: brushColor })}
class="pen-wrapper edgeless-highlighter-button" ?data-active="${type === 'brush'}"
?data-active="${pen === 'highlighter'}" >
style=${styleMap({ color: highlighterColor })} ${brushIcon}
</div>
</edgeless-tool-icon-button>
<edgeless-tool-icon-button
class="edgeless-highlighter-button"
.tooltip=${html`<affine-tooltip-content-with-shortcut
data-tip="${penInfoMap.highlighter.tip}"
data-shortcut="${penInfoMap.highlighter.shortcut}"
></affine-tooltip-content-with-shortcut>`}
.tooltipOffset=${20}
.hover=${false}
@click=${() => this._onPickPen('highlighter')} @click=${() => this._onPickPen('highlighter')}
> >
${highlighterIcon} <div
</div> class="pen-wrapper"
style=${styleMap({ color: highlighterColor })}
?data-active="${type === 'highlighter'}"
>
${highlighterIcon}
</div>
</edgeless-tool-icon-button>
<menu-divider .vertical=${true}></menu-divider> <menu-divider .vertical=${true}></menu-divider>
</div> </div>
<div class="menu-content"> <div class="menu-content">
<edgeless-color-panel <edgeless-color-panel
class="one-way" class="one-way"
@select=${this._onPickColor} @select=${this._onPickColor}
.value=${currentColor} .value=${color}
.theme=${theme} .theme=${theme}
.palettes=${DefaultTheme.StrokeColorShortPalettes} .palettes=${DefaultTheme.StrokeColorShortPalettes}
.shouldKeepColor=${true} .shouldKeepColor=${true}
@@ -135,14 +170,20 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
accessor onChange!: (props: Record<string, unknown>) => void; accessor onChange!: (props: Record<string, unknown>) => void;
@property({ attribute: false }) @property({ attribute: false })
accessor colors$!: Signal<PenMap<string>>; accessor colors$!: ReadonlySignal<PenMap<string>>;
@property({ attribute: false }) @property({ attribute: false })
accessor color$!: Signal<string>; accessor penIconMap$!: ReadonlySignal<PenMap<TemplateResult>>;
@property({ attribute: false }) @property({ attribute: false })
accessor pen$!: Signal<Pen>; accessor pen$!: Signal<Pen>;
@property({ attribute: false }) @property({ attribute: false })
accessor penIconMap$!: Signal<PenMap<TemplateResult>>; accessor penInfo$!: ReadonlySignal<{
type: Pen;
color: string;
icon: TemplateResult<1>;
tip: string;
shortcut: string;
}>;
} }
@@ -10,7 +10,7 @@ import { css, html, LitElement, nothing } from 'lit';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js'; import { when } from 'lit/directives/when.js';
import { penIconMap } from './icons'; import { penIconMap, penInfoMap } from './consts';
import type { Pen } from './types'; import type { Pen } from './types';
export class EdgelessPenToolButton extends EdgelessToolbarToolMixin( export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
@@ -81,6 +81,18 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
return this.penIconMap$.value[pen]; return this.penIconMap$.value[pen];
}); });
private readonly penInfo$ = computed(() => {
const type = this.pen$.value;
const icon = this.penIcon$.value;
const color = this.color$.value;
return {
...penInfoMap[type],
color,
icon,
type,
};
});
private readonly pen$ = signal<Pen>('brush'); private readonly pen$ = signal<Pen>('brush');
override enableActiveBackground = true; override enableActiveBackground = true;
@@ -89,9 +101,20 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
override firstUpdated() { override firstUpdated() {
this.disposables.add( this.disposables.add(
this.gfx.tool.currentToolName$.subscribe(tool => { this.gfx.tool.currentToolName$.subscribe(name => {
if (this.type.map(String).includes(tool)) return; const tool = this.type.find(t => t === name);
this.tryDisposePopper(); if (!tool) {
this.tryDisposePopper();
return;
}
if (tool !== this.pen$.peek()) {
this.pen$.value = tool;
}
if (this.active) return;
this._togglePenMenu();
}) })
); );
} }
@@ -101,10 +124,10 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
!this.active && this.setEdgelessTool(this.pen$.peek()); !this.active && this.setEdgelessTool(this.pen$.peek());
const menu = this.createPopper('edgeless-pen-menu', this); const menu = this.createPopper('edgeless-pen-menu', this);
Object.assign(menu.element, { Object.assign(menu.element, {
color$: this.color$,
colors$: this.colors$, colors$: this.colors$,
pen$: this.pen$,
penIconMap$: this.penIconMap$, penIconMap$: this.penIconMap$,
pen$: this.pen$,
penInfo$: this.penInfo$,
edgeless: this.edgeless, edgeless: this.edgeless,
onChange: (props: Record<string, unknown>) => { onChange: (props: Record<string, unknown>) => {
const pen = this.pen$.peek(); const pen = this.pen$.peek();
@@ -117,20 +140,22 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
override render() { override render() {
const { const {
active, active,
penIcon$: { value: icon }, penInfo$: {
color$: { value: color }, value: { type, color, icon, tip, shortcut },
},
} = this; } = this;
return html` return html`
<edgeless-toolbar-button <edgeless-toolbar-button
class="edgeless-pen-button" class="edgeless-pen-button"
data-drawing-tool="${type}"
.tooltip=${when( .tooltip=${when(
this.popper, this.popper,
() => nothing, () => nothing,
() => () =>
html`<affine-tooltip-content-with-shortcut html`<affine-tooltip-content-with-shortcut
data-tip="${'Pen'}" data-tip="${tip}"
data-shortcut="${'P'}" data-shortcut="${shortcut}"
></affine-tooltip-content-with-shortcut>` ></affine-tooltip-content-with-shortcut>`
)} )}
.tooltipOffset=${4} .tooltipOffset=${4}
@@ -57,3 +57,28 @@ test('should exit drawing tools menu when Escape is pressed', async ({
await expect(drawingToolsMenu).toBeHidden(); await expect(drawingToolsMenu).toBeHidden();
}); });
test('should enter highlighter tool when `Shift + P` is pressed', async ({
page,
}) => {
const drawingToolButton = page.locator('.edgeless-pen-button');
const drawingToolsMenu = page.locator('edgeless-pen-menu');
await expect(drawingToolButton).toHaveAttribute('data-drawing-tool', 'brush');
await expect(drawingToolsMenu).toBeHidden();
await page.keyboard.press('Shift+P');
await expect(drawingToolButton).toHaveAttribute(
'data-drawing-tool',
'highlighter'
);
await expect(drawingToolsMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(drawingToolsMenu).toBeHidden();
await expect(drawingToolButton).toHaveAttribute(
'data-drawing-tool',
'highlighter'
);
});
@@ -211,7 +211,7 @@ export async function locatorEdgelessToolButton(
switch (type) { switch (type) {
case 'brush': case 'brush':
case 'highlighter': case 'highlighter':
buttonType = 'div'; buttonType = 'edgeless-tool-icon-button';
break; break;
case 'pen': case 'pen':
case 'text': case 'text':
+1 -1
View File
@@ -289,7 +289,7 @@ export async function locateEdgelessToolButton(
switch (type) { switch (type) {
case 'brush': case 'brush':
case 'highlighter': case 'highlighter':
buttonType = 'div'; buttonType = 'edgeless-tool-icon-button';
break; break;
case 'pen': case 'pen':
case 'text': case 'text':