[improvement] quick actions (#1922)

This PR:

- improves the quick actions bar, enabling undo / redo actions when the
eraser is selected.
- for actions that effect selected shapes, calling the action when the
select tool is not selected will select the select tool and run the
action
- actions that effect selected shapes are hidden from the menu when the
select tool is not selected

### Change Type

- [x] `major`

### Test Plan

1. Select the eraser tool, the undo / redo buttons should still be
there.

1. Select two shapes
2. Select the draw tool
3. The menu should not display most options, e.g. cut or paste, but
should display undo / redo
4. Press Shift+H
5. The shapes should not move, but the select tool should be selected
again

### Release Notes

- Improve the menu / kbds behavior when select tool is not active
This commit is contained in:
Steve Ruiz 2023-09-19 16:33:39 +01:00 committed by GitHub
parent 386a2396d1
commit b6ebe1e274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 387 additions and 251 deletions

View file

@ -41,15 +41,20 @@ export async function setupPage(page: PlaywrightTestArgs['page']) {
} }
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) { export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
// delete everything
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')
// create shapes
await page.keyboard.press('r') await page.keyboard.press('r')
await page.mouse.click(200, 200) await page.mouse.click(200, 200)
await page.keyboard.press('r') await page.keyboard.press('r')
await page.mouse.click(200, 250) await page.mouse.click(200, 250)
await page.keyboard.press('r') await page.keyboard.press('r')
await page.mouse.click(250, 300) await page.mouse.click(250, 300)
await page.evaluate(() => {
editor.selectNone() // deselect everything
}) await page.evaluate(() => editor.selectNone())
} }
export async function cleanupPage(page: PlaywrightTestArgs['page']) { export async function cleanupPage(page: PlaywrightTestArgs['page']) {

View file

@ -64,8 +64,21 @@ test.describe('Keyboard Shortcuts', () => {
}) })
} }
}) })
})
test.describe('Keyboard Shortcuts', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
// Make some shapes
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(250, 250)
await page.keyboard.press('v')
})
test.describe('actions', () => {
test('Zoom in', async () => { test('Zoom in', async () => {
await page.keyboard.press('Control+=') await page.keyboard.press('Control+=')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({ expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
@ -134,8 +147,6 @@ test.describe('Keyboard Shortcuts', () => {
// data: { source: 'kbd' }, // data: { source: 'kbd' },
// }) // })
/* ------------------- Preferences ------------------ */
test('Toggle grid mode', async () => { test('Toggle grid mode', async () => {
await page.keyboard.press("Control+'") await page.keyboard.press("Control+'")
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({ expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
@ -159,6 +170,13 @@ test.describe('Keyboard Shortcuts', () => {
data: { source: 'kbd' }, data: { source: 'kbd' },
}) })
}) })
})
test.describe('Actions on shapes', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
/* -------------- Operations on Shapes -------------- */ /* -------------- Operations on Shapes -------------- */
@ -235,20 +253,6 @@ test.describe('Keyboard Shortcuts', () => {
data: { source: 'kbd' }, data: { source: 'kbd' },
}) })
// delete — backspace
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// delete — ⌫
await page.keyboard.press('Delete')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// align left — Alt+A // align left — Alt+A
await page.keyboard.press('Alt+a') await page.keyboard.press('Alt+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({ expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
@ -277,6 +281,30 @@ test.describe('Keyboard Shortcuts', () => {
data: { operation: 'bottom', source: 'kbd' }, data: { operation: 'bottom', source: 'kbd' },
}) })
// delete — backspace
await page.keyboard.press('Control+a') // selected
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// delete — ⌫
// Make some shapes and select them
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(250, 250)
await page.keyboard.press('v')
await page.keyboard.press('Control+a')
await page.keyboard.press('Delete')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
/* ---------------------- Misc ---------------------- */ /* ---------------------- Misc ---------------------- */
// toggle lock // toggle lock
@ -306,7 +334,6 @@ test.describe('Keyboard Shortcuts', () => {
}) })
}) })
}) })
})
test.describe('Context menu', async () => { test.describe('Context menu', async () => {
test.beforeEach(async ({ browser }) => { test.beforeEach(async ({ browser }) => {

View file

@ -97,6 +97,7 @@ test.describe('Shape Tools', () => {
}) })
test('creates shapes clickable tools', async () => { test('creates shapes clickable tools', async () => {
await page.keyboard.press('v')
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
expect(await getAllShapeTypes(page)).toEqual([]) expect(await getAllShapeTypes(page)).toEqual([])
@ -124,6 +125,7 @@ test.describe('Shape Tools', () => {
// Reset for next time // Reset for next time
await page.mouse.click(0, 0) // to ensure we're not focused await page.mouse.click(0, 0) // to ensure we're not focused
await page.keyboard.press('v') // go to the select tool
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
} }
@ -162,6 +164,7 @@ test.describe('Shape Tools', () => {
// Reset for next time // Reset for next time
await page.mouse.click(0, 0) // to ensure we're not focused await page.mouse.click(0, 0) // to ensure we're not focused
await page.keyboard.press('v')
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
} }

View file

@ -10,13 +10,11 @@ export const DuplicateButton = track(function DuplicateButton() {
const msg = useTranslation() const msg = useTranslation()
const action = actions['duplicate'] const action = actions['duplicate']
const noSelected = editor.selectedShapeIds.length <= 0
return ( return (
<Button <Button
icon={action.icon} icon={action.icon}
onClick={() => action.onSelect('quick-actions')} onClick={() => action.onSelect('quick-actions')}
disabled={noSelected} disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`} title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
smallIcon smallIcon
/> />

View file

@ -15,15 +15,13 @@ export const MenuZone = track(function MenuZone() {
const breakpoint = useBreakpoint() const breakpoint = useBreakpoint()
const isReadonly = useReadonly() const isReadonly = useReadonly()
const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser')
return ( return (
<div className="tlui-menu-zone"> <div className="tlui-menu-zone">
<div className="tlui-menu-zone__controls"> <div className="tlui-menu-zone__controls">
<Menu /> <Menu />
<div className="tlui-menu-zone__divider" /> <div className="tlui-menu-zone__divider" />
<PageMenu /> <PageMenu />
{breakpoint >= 6 && showQuickActions && ( {breakpoint >= 6 && !isReadonly && !editor.isInAny('hand', 'zoom') && (
<> <>
<div className="tlui-menu-zone__divider" /> <div className="tlui-menu-zone__divider" />
<UndoButton /> <UndoButton />

View file

@ -110,13 +110,12 @@ export const Toolbar = memo(function Toolbar() {
<div className="tlui-toolbar"> <div className="tlui-toolbar">
<div className="tlui-toolbar__inner"> <div className="tlui-toolbar__inner">
<div className="tlui-toolbar__left"> <div className="tlui-toolbar__left">
{!isReadonly && ( {!isReadonly && breakpoint < 6 && !editor.isInAny('hand', 'zoom') && (
<div <div
className={classNames('tlui-toolbar__extras', { className={classNames('tlui-toolbar__extras', {
'tlui-toolbar__extras__hidden': !showExtraActions, 'tlui-toolbar__extras__hidden': !showExtraActions,
})} })}
> >
{breakpoint < 6 && (
<div className="tlui-toolbar__extras__controls"> <div className="tlui-toolbar__extras__controls">
<UndoButton /> <UndoButton />
<RedoButton /> <RedoButton />
@ -124,7 +123,6 @@ export const Toolbar = memo(function Toolbar() {
<DuplicateButton /> <DuplicateButton />
<ActionsMenu /> <ActionsMenu />
</div> </div>
)}
<ToggleToolLockedButton activeToolId={activeToolId} /> <ToggleToolLockedButton activeToolId={activeToolId} />
</div> </div>
)} )}

View file

@ -15,13 +15,11 @@ export const TrashButton = track(function TrashButton() {
if (isReadonly) return null if (isReadonly) return null
const noSelected = editor.selectedShapeIds.length <= 0
return ( return (
<Button <Button
icon={action.icon} icon={action.icon}
onClick={() => action.onSelect('quick-actions')} onClick={() => action.onSelect('quick-actions')}
disabled={noSelected} disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`} title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
smallIcon smallIcon
/> />

View file

@ -83,6 +83,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
// should this be a useMemo? looks like it doesn't actually deref any reactive values // should this be a useMemo? looks like it doesn't actually deref any reactive values
const actions = React.useMemo<TLUiActionsContextType>(() => { const actions = React.useMemo<TLUiActionsContextType>(() => {
function mustGoBackToSelectToolFirst() {
if (!editor.isIn('select')) {
editor.complete()
editor.setCurrentTool('select')
return false // false will still let the action happen, true will stop it
// todo: remove this return value once we're suuuuure
}
return false
}
function hasSelectedShapes() {
return editor.selectedShapeIds.length > 0
}
const actions = makeActions([ const actions = makeActions([
{ {
id: 'edit-link', id: 'edit-link',
@ -90,6 +105,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'link', icon: 'link',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('edit-link', { source }) trackEvent('edit-link', { source })
editor.mark('edit-link') editor.mark('edit-link')
addDialog({ component: EditLinkDialog }) addDialog({ component: EditLinkDialog })
@ -209,6 +227,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.toggle-auto-size', label: 'action.toggle-auto-size',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('toggle-auto-size', { source }) trackEvent('toggle-auto-size', { source })
editor.mark('toggling auto size') editor.mark('toggling auto size')
editor.updateShapes( editor.updateShapes(
@ -273,6 +294,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.convert-to-bookmark', label: 'action.convert-to-bookmark',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
editor.batch(() => { editor.batch(() => {
trackEvent('convert-to-bookmark', { source }) trackEvent('convert-to-bookmark', { source })
const shapes = editor.selectedShapes const shapes = editor.selectedShapes
@ -314,6 +338,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.convert-to-embed', label: 'action.convert-to-embed',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('convert-to-embed', { source }) trackEvent('convert-to-embed', { source })
editor.batch(() => { editor.batch(() => {
@ -369,7 +396,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'duplicate', icon: 'duplicate',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (editor.currentToolId !== 'select') return if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('duplicate-shapes', { source }) trackEvent('duplicate-shapes', { source })
const ids = editor.selectedShapeIds const ids = editor.selectedShapeIds
const commonBounds = Box2d.Common(compact(ids.map((id) => editor.getShapePageBounds(id)))) const commonBounds = Box2d.Common(compact(ids.map((id) => editor.getShapePageBounds(id))))
@ -393,6 +422,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'ungroup', icon: 'ungroup',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('ungroup-shapes', { source }) trackEvent('ungroup-shapes', { source })
editor.mark('ungroup') editor.mark('ungroup')
editor.ungroupShapes(editor.selectedShapeIds) editor.ungroupShapes(editor.selectedShapeIds)
@ -405,6 +437,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'group', icon: 'group',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('group-shapes', { source }) trackEvent('group-shapes', { source })
const { onlySelectedShape } = editor const { onlySelectedShape } = editor
if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) { if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) {
@ -423,6 +458,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'align-left', icon: 'align-left',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('align-shapes', { operation: 'left', source }) trackEvent('align-shapes', { operation: 'left', source })
editor.mark('align left') editor.mark('align left')
editor.alignShapes(editor.selectedShapeIds, 'left') editor.alignShapes(editor.selectedShapeIds, 'left')
@ -436,6 +474,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'align-center-horizontal', icon: 'align-center-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('align-shapes', { operation: 'center-horizontal', source }) trackEvent('align-shapes', { operation: 'center-horizontal', source })
editor.mark('align center horizontal') editor.mark('align center horizontal')
editor.alignShapes(editor.selectedShapeIds, 'center-horizontal') editor.alignShapes(editor.selectedShapeIds, 'center-horizontal')
@ -448,6 +489,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'align-right', icon: 'align-right',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('align-shapes', { operation: 'right', source }) trackEvent('align-shapes', { operation: 'right', source })
editor.mark('align right') editor.mark('align right')
editor.alignShapes(editor.selectedShapeIds, 'right') editor.alignShapes(editor.selectedShapeIds, 'right')
@ -461,6 +505,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'align-center-vertical', icon: 'align-center-vertical',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('align-shapes', { operation: 'center-vertical', source }) trackEvent('align-shapes', { operation: 'center-vertical', source })
editor.mark('align center vertical') editor.mark('align center vertical')
editor.alignShapes(editor.selectedShapeIds, 'center-vertical') editor.alignShapes(editor.selectedShapeIds, 'center-vertical')
@ -473,6 +520,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?W', kbd: '?W',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('align-shapes', { operation: 'top', source }) trackEvent('align-shapes', { operation: 'top', source })
editor.mark('align top') editor.mark('align top')
editor.alignShapes(editor.selectedShapeIds, 'top') editor.alignShapes(editor.selectedShapeIds, 'top')
@ -485,6 +535,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?S', kbd: '?S',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('align-shapes', { operation: 'bottom', source }) trackEvent('align-shapes', { operation: 'bottom', source })
editor.mark('align bottom') editor.mark('align bottom')
editor.alignShapes(editor.selectedShapeIds, 'bottom') editor.alignShapes(editor.selectedShapeIds, 'bottom')
@ -498,6 +551,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?!h', kbd: '?!h',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('distribute-shapes', { operation: 'horizontal', source }) trackEvent('distribute-shapes', { operation: 'horizontal', source })
editor.mark('distribute horizontal') editor.mark('distribute horizontal')
editor.distributeShapes(editor.selectedShapeIds, 'horizontal') editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
@ -511,6 +567,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?!V', kbd: '?!V',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('distribute-shapes', { operation: 'vertical', source }) trackEvent('distribute-shapes', { operation: 'vertical', source })
editor.mark('distribute vertical') editor.mark('distribute vertical')
editor.distributeShapes(editor.selectedShapeIds, 'vertical') editor.distributeShapes(editor.selectedShapeIds, 'vertical')
@ -523,6 +582,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'stretch-horizontal', icon: 'stretch-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('stretch-shapes', { operation: 'horizontal', source }) trackEvent('stretch-shapes', { operation: 'horizontal', source })
editor.mark('stretch horizontal') editor.mark('stretch horizontal')
editor.stretchShapes(editor.selectedShapeIds, 'horizontal') editor.stretchShapes(editor.selectedShapeIds, 'horizontal')
@ -535,6 +597,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'stretch-vertical', icon: 'stretch-vertical',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('stretch-shapes', { operation: 'vertical', source }) trackEvent('stretch-shapes', { operation: 'vertical', source })
editor.mark('stretch vertical') editor.mark('stretch vertical')
editor.stretchShapes(editor.selectedShapeIds, 'vertical') editor.stretchShapes(editor.selectedShapeIds, 'vertical')
@ -547,6 +612,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '!h', kbd: '!h',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('flip-shapes', { operation: 'horizontal', source }) trackEvent('flip-shapes', { operation: 'horizontal', source })
editor.mark('flip horizontal') editor.mark('flip horizontal')
editor.flipShapes(editor.selectedShapeIds, 'horizontal') editor.flipShapes(editor.selectedShapeIds, 'horizontal')
@ -559,6 +627,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '!v', kbd: '!v',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('flip-shapes', { operation: 'vertical', source }) trackEvent('flip-shapes', { operation: 'vertical', source })
editor.mark('flip vertical') editor.mark('flip vertical')
editor.flipShapes(editor.selectedShapeIds, 'vertical') editor.flipShapes(editor.selectedShapeIds, 'vertical')
@ -570,6 +641,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'pack', icon: 'pack',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('pack-shapes', { source }) trackEvent('pack-shapes', { source })
editor.mark('pack') editor.mark('pack')
editor.packShapes(editor.selectedShapeIds, 16) editor.packShapes(editor.selectedShapeIds, 16)
@ -582,6 +656,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'stack-vertical', icon: 'stack-vertical',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('stack-shapes', { operation: 'vertical', source }) trackEvent('stack-shapes', { operation: 'vertical', source })
editor.mark('stack-vertical') editor.mark('stack-vertical')
editor.stackShapes(editor.selectedShapeIds, 'vertical', 16) editor.stackShapes(editor.selectedShapeIds, 'vertical', 16)
@ -594,6 +671,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'stack-horizontal', icon: 'stack-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('stack-shapes', { operation: 'horizontal', source }) trackEvent('stack-shapes', { operation: 'horizontal', source })
editor.mark('stack-horizontal') editor.mark('stack-horizontal')
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 16) editor.stackShapes(editor.selectedShapeIds, 'horizontal', 16)
@ -606,6 +686,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'bring-to-front', icon: 'bring-to-front',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('reorder-shapes', { operation: 'toFront', source }) trackEvent('reorder-shapes', { operation: 'toFront', source })
editor.mark('bring to front') editor.mark('bring to front')
editor.bringToFront(editor.selectedShapeIds) editor.bringToFront(editor.selectedShapeIds)
@ -618,6 +701,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?]', kbd: '?]',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('reorder-shapes', { operation: 'forward', source }) trackEvent('reorder-shapes', { operation: 'forward', source })
editor.mark('bring forward') editor.mark('bring forward')
editor.bringForward(editor.selectedShapeIds) editor.bringForward(editor.selectedShapeIds)
@ -630,6 +716,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?[', kbd: '?[',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('reorder-shapes', { operation: 'backward', source }) trackEvent('reorder-shapes', { operation: 'backward', source })
editor.mark('send backward') editor.mark('send backward')
editor.sendBackward(editor.selectedShapeIds) editor.sendBackward(editor.selectedShapeIds)
@ -642,6 +731,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '[', kbd: '[',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('reorder-shapes', { operation: 'toBack', source }) trackEvent('reorder-shapes', { operation: 'toBack', source })
editor.mark('send to back') editor.mark('send to back')
editor.sendToBack(editor.selectedShapeIds) editor.sendToBack(editor.selectedShapeIds)
@ -653,6 +745,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '$x', kbd: '$x',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
editor.mark('cut') editor.mark('cut')
cut(source) cut(source)
}, },
@ -663,6 +758,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '$c', kbd: '$c',
readonlyOk: true, readonlyOk: true,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
copy(source) copy(source)
}, },
}, },
@ -688,11 +786,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true, readonlyOk: true,
onSelect(source) { onSelect(source) {
editor.batch(() => { editor.batch(() => {
if (mustGoBackToSelectToolFirst()) return
trackEvent('select-all-shapes', { source }) trackEvent('select-all-shapes', { source })
if (editor.currentToolId !== 'select') {
editor.cancel()
editor.setCurrentTool('select')
}
editor.mark('select all kbd') editor.mark('select all kbd')
editor.selectAll() editor.selectAll()
@ -704,6 +800,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.select-none', label: 'action.select-none',
readonlyOk: true, readonlyOk: true,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('select-none-shapes', { source }) trackEvent('select-none-shapes', { source })
editor.mark('select none') editor.mark('select none')
editor.selectNone() editor.selectNone()
@ -716,7 +815,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'trash', icon: 'trash',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (editor.currentToolId !== 'select') return if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('delete-shapes', { source }) trackEvent('delete-shapes', { source })
editor.mark('delete') editor.mark('delete')
editor.deleteShapes(editor.selectedShapeIds) editor.deleteShapes(editor.selectedShapeIds)
@ -728,7 +829,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'rotate-cw', icon: 'rotate-cw',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (editor.selectedShapeIds.length === 0) return if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('rotate-cw', { source }) trackEvent('rotate-cw', { source })
editor.mark('rotate-cw') editor.mark('rotate-cw')
const offset = editor.selectionRotation % (TAU / 2) const offset = editor.selectionRotation % (TAU / 2)
@ -742,7 +845,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'rotate-ccw', icon: 'rotate-ccw',
readonlyOk: false, readonlyOk: false,
onSelect(source) { onSelect(source) {
if (editor.selectedShapeIds.length === 0) return if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('rotate-ccw', { source }) trackEvent('rotate-ccw', { source })
editor.mark('rotate-ccw') editor.mark('rotate-ccw')
const offset = editor.selectionRotation % (TAU / 2) const offset = editor.selectionRotation % (TAU / 2)
@ -797,6 +902,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '!2', kbd: '!2',
readonlyOk: true, readonlyOk: true,
onSelect(source) { onSelect(source) {
if (!hasSelectedShapes()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('zoom-to-selection', { source }) trackEvent('zoom-to-selection', { source })
editor.zoomToSelection({ duration: ANIMATION_MEDIUM_MS }) editor.zoomToSelection({ duration: ANIMATION_MEDIUM_MS })
}, },

View file

@ -1,8 +1,9 @@
import { Editor, TLBookmarkShape, TLEmbedShape, compact, useEditor, useValue } from '@tldraw/editor' import { Editor, TLBookmarkShape, TLEmbedShape, useEditor, useValue } from '@tldraw/editor'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { getEmbedInfo } from '../../utils/embeds' import { getEmbedInfo } from '../../utils/embeds'
import { import {
TLUiMenuSchema, TLUiMenuSchema,
compactMenuItems,
menuCustom, menuCustom,
menuGroup, menuGroup,
menuItem, menuItem,
@ -108,7 +109,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
) )
const menuSchema = useMemo<TLUiMenuSchema>(() => { const menuSchema = useMemo<TLUiMenuSchema>(() => {
const menuSchema = compact([ const menuSchema = compactMenuItems([
menuGroup( menuGroup(
'menu', 'menu',
menuSubmenu( menuSubmenu(