Merge pull request #48 from tldraw/tool-keyboard-shortcuts

Tool keyboard shortcuts
This commit is contained in:
Steve Ruiz 2021-07-13 22:42:55 +01:00 committed by GitHub
commit 77403941f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 190 additions and 99 deletions

View file

@ -216,13 +216,12 @@ export const Item = styled('button', {
pointerEvents: 'all', pointerEvents: 'all',
cursor: 'pointer', cursor: 'pointer',
'&:focus': {
backgroundColor: '$hover',
},
'&:hover:not(:disabled)': { '&:hover:not(:disabled)': {
backgroundColor: '$hover', backgroundColor: '$hover',
'& svg': {
stroke: '$text',
fill: '$text',
strokeWidth: '0',
},
}, },
'&:disabled': { '&:disabled': {
@ -247,6 +246,16 @@ export const Item = styled('button', {
}, },
}) })
export const ShortcutKey = styled('span', {
fontSize: '$0',
width: '16px',
height: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '1px 1px 0px rgba(0,0,0,.5)',
})
export const IconWrapper = styled('div', { export const IconWrapper = styled('div', {
height: '100%', height: '100%',
borderRadius: '4px', borderRadius: '4px',

View file

@ -1,37 +0,0 @@
import { IconButton } from 'components/shared'
import { strokes } from 'state/shape-styles'
import { ColorStyle } from 'types'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { Square } from 'react-feather'
import { DropdownContent } from '../shared'
import { memo } from 'react'
import state from 'state'
import useTheme from 'hooks/useTheme'
function handleColorChange(
e: Event & { currentTarget: { value: ColorStyle } }
): void {
state.send('CHANGED_STYLE', { color: e.currentTarget.value })
}
function ColorContent(): JSX.Element {
const { theme } = useTheme()
return (
<DropdownContent sideOffset={8} side="bottom">
{Object.keys(strokes[theme]).map((color: ColorStyle) => (
<DropdownMenu.DropdownMenuItem
as={IconButton}
key={color}
title={color}
value={color}
onSelect={handleColorChange}
>
<Square fill={strokes[theme][color]} stroke="none" size="22" />
</DropdownMenu.DropdownMenuItem>
))}
</DropdownContent>
)
}
export default memo(ColorContent)

View file

@ -1,11 +1,15 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { breakpoints, IconButton } from 'components/shared' import { breakpoints, IconButton } from 'components/shared'
import Tooltip from 'components/tooltip' import Tooltip from 'components/tooltip'
import { fills, strokes } from 'state/shape-styles' import { strokes } from 'state/shape-styles'
import { useSelector } from 'state' import state, { useSelector } from 'state'
import ColorContent from './color-content' import { BoxIcon, Item, DropdownContent } from '../shared'
import { BoxIcon } from '../shared'
import useTheme from 'hooks/useTheme' import useTheme from 'hooks/useTheme'
import { ColorStyle } from 'types'
function handleColorChange(color: ColorStyle): void {
state.send('CHANGED_STYLE', { color })
}
export default function QuickColorSelect(): JSX.Element { export default function QuickColorSelect(): JSX.Element {
const color = useSelector((s) => s.values.selectedStyle.color) const color = useSelector((s) => s.values.selectedStyle.color)
@ -15,10 +19,33 @@ export default function QuickColorSelect(): JSX.Element {
<DropdownMenu.Root dir="ltr"> <DropdownMenu.Root dir="ltr">
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}> <DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
<Tooltip label="Color"> <Tooltip label="Color">
<BoxIcon fill={fills[theme][color]} stroke={strokes[theme][color]} /> <BoxIcon
fill={strokes[theme][color]}
stroke={strokes[theme][color]}
/>
</Tooltip> </Tooltip>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<ColorContent />
<DropdownMenu.DropdownMenuRadioGroup
value={color}
as={DropdownContent}
onValueChange={handleColorChange}
sideOffset={8}
>
{Object.keys(strokes[theme]).map((colorStyle: ColorStyle) => (
<DropdownMenu.DropdownMenuRadioItem
as={Item}
key={colorStyle}
title={colorStyle}
value={colorStyle}
>
<BoxIcon
fill={strokes[theme][colorStyle]}
stroke={strokes[theme][colorStyle]}
/>
</DropdownMenu.DropdownMenuRadioItem>
))}
</DropdownMenu.DropdownMenuRadioGroup>
</DropdownMenu.Root> </DropdownMenu.Root>
) )
} }

View file

@ -1,7 +1,7 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { breakpoints, IconButton } from 'components/shared' import { breakpoints, IconButton } from 'components/shared'
import Tooltip from 'components/tooltip' import Tooltip from 'components/tooltip'
import { memo } from 'react' import React, { memo } from 'react'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import { DashStyle } from 'types' import { DashStyle } from 'types'
import { import {
@ -20,10 +20,8 @@ const dashes = {
[DashStyle.Dotted]: <DashDottedIcon />, [DashStyle.Dotted]: <DashDottedIcon />,
} }
function changeDashStyle( function changeDashStyle(dash: DashStyle): void {
e: Event & { currentTarget: { value: DashStyle } } state.send('CHANGED_STYLE', { dash })
): void {
state.send('CHANGED_STYLE', { dash: e.currentTarget.value })
} }
function QuickdashSelect(): JSX.Element { function QuickdashSelect(): JSX.Element {
@ -34,19 +32,24 @@ function QuickdashSelect(): JSX.Element {
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}> <DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
<Tooltip label="Dash">{dashes[dash]}</Tooltip> <Tooltip label="Dash">{dashes[dash]}</Tooltip>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownContent sideOffset={8} direction="vertical"> <DropdownMenu.DropdownMenuRadioGroup
as={DropdownContent}
sideOffset={8}
direction="vertical"
value={dash}
onValueChange={changeDashStyle}
>
{Object.keys(DashStyle).map((dashStyle: DashStyle) => ( {Object.keys(DashStyle).map((dashStyle: DashStyle) => (
<DropdownMenu.DropdownMenuItem <DropdownMenu.DropdownMenuRadioItem
as={Item} as={Item}
key={dashStyle} key={dashStyle}
isActive={dash === dashStyle} isActive={dash === dashStyle}
onSelect={changeDashStyle}
value={dashStyle} value={dashStyle}
> >
{dashes[dashStyle]} {dashes[dashStyle]}
</DropdownMenu.DropdownMenuItem> </DropdownMenu.DropdownMenuRadioItem>
))} ))}
</DropdownContent> </DropdownMenu.DropdownMenuRadioGroup>
</DropdownMenu.Root> </DropdownMenu.Root>
) )
} }

View file

@ -13,10 +13,8 @@ const sizes = {
[SizeStyle.Large]: 22, [SizeStyle.Large]: 22,
} }
function handleSizeChange( function changeSizeStyle(size: SizeStyle): void {
e: Event & { currentTarget: { value: SizeStyle } } state.send('CHANGED_STYLE', { size })
): void {
state.send('CHANGED_STYLE', { size: e.currentTarget.value })
} }
function QuickSizeSelect(): JSX.Element { function QuickSizeSelect(): JSX.Element {
@ -29,19 +27,24 @@ function QuickSizeSelect(): JSX.Element {
<Circle size={sizes[size]} stroke="none" fill="currentColor" /> <Circle size={sizes[size]} stroke="none" fill="currentColor" />
</Tooltip> </Tooltip>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownContent sideOffset={8} direction="vertical"> <DropdownMenu.DropdownMenuRadioGroup
as={DropdownContent}
sideOffset={8}
direction="vertical"
value={size}
onValueChange={changeSizeStyle}
>
{Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => ( {Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
<DropdownMenu.DropdownMenuItem <DropdownMenu.DropdownMenuRadioItem
key={sizeStyle} key={sizeStyle}
as={Item} as={Item}
isActive={size === sizeStyle} isActive={size === sizeStyle}
value={sizeStyle} value={sizeStyle}
onSelect={handleSizeChange}
> >
<Circle size={sizes[sizeStyle]} /> <Circle size={sizes[sizeStyle]} />
</DropdownMenu.DropdownMenuItem> </DropdownMenu.DropdownMenuRadioItem>
))} ))}
</DropdownContent> </DropdownMenu.DropdownMenuRadioGroup>
</DropdownMenu.Root> </DropdownMenu.Root>
) )
} }

View file

@ -2,14 +2,17 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import state from 'state' import state from 'state'
import inputs from 'state/inputs' import inputs from 'state/inputs'
import { MoveType } from 'types' import { ColorStyle, MoveType, SizeStyle } from 'types'
import { metaKey } from 'utils' import { metaKey } from 'utils'
export default function useKeyboardEvents() { export default function useKeyboardEvents() {
useEffect(() => { useEffect(() => {
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
const info = inputs.keydown(e)
const meta = metaKey(e)
if ( if (
metaKey(e) && meta &&
![ ![
'a', 'a',
'i', 'i',
@ -25,9 +28,88 @@ export default function useKeyboardEvents() {
e.preventDefault() e.preventDefault()
} }
const info = inputs.keydown(e)
switch (e.key) { switch (e.key) {
case '1': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Black })
break
}
if (e.altKey) {
state.send('CHANGED_STYLE', { size: SizeStyle.Small })
break
}
state.send('SELECTED_SELECT_TOOL', info)
break
}
case '2': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.White })
break
}
if (e.altKey) {
state.send('CHANGED_STYLE', { size: SizeStyle.Medium })
break
}
state.send('SELECTED_DRAW_TOOL', info)
break
}
case '3': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Green })
break
}
if (e.altKey) {
state.send('CHANGED_STYLE', { size: SizeStyle.Large })
break
}
state.send('SELECTED_RECTANGLE_TOOL', info)
break
}
case '4': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Blue })
}
state.send('SELECTED_ELLIPSE_TOOL', info)
break
}
case '5': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Indigo })
break
}
state.send('SELECTED_ARROW_TOOL', info)
break
}
case '6': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Violet })
break
}
state.send('SELECTED_TEXT_TOOL', info)
break
}
case '7': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Red })
break
}
state.send('TOGGLED_TOOL_LOCK', info)
break
}
case '8': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Orange })
break
}
break
}
case '9': {
if (meta) {
state.send('CHANGED_STYLE', { color: ColorStyle.Yellow })
break
}
break
}
case 'ArrowUp': { case 'ArrowUp': {
state.send('NUDGED', { delta: [0, -1], ...info }) state.send('NUDGED', { delta: [0, -1], ...info })
break break
@ -82,7 +164,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'z': { case 'z': {
if (metaKey(e)) { if (meta) {
if (e.shiftKey) { if (e.shiftKey) {
state.send('REDO', info) state.send('REDO', info)
} else { } else {
@ -92,7 +174,7 @@ export default function useKeyboardEvents() {
break break
} }
case '': { case '': {
if (metaKey(e)) { if (meta) {
state.send('MOVED', { state.send('MOVED', {
...info, ...info,
type: MoveType.ToFront, type: MoveType.ToFront,
@ -101,7 +183,7 @@ export default function useKeyboardEvents() {
break break
} }
case '“': { case '“': {
if (metaKey(e)) { if (meta) {
state.send('MOVED', { state.send('MOVED', {
...info, ...info,
type: MoveType.ToBack, type: MoveType.ToBack,
@ -110,7 +192,7 @@ export default function useKeyboardEvents() {
break break
} }
case ']': { case ']': {
if (metaKey(e)) { if (meta) {
state.send('MOVED', { state.send('MOVED', {
...info, ...info,
type: MoveType.Forward, type: MoveType.Forward,
@ -119,7 +201,7 @@ export default function useKeyboardEvents() {
break break
} }
case '[': { case '[': {
if (metaKey(e)) { if (meta) {
state.send('MOVED', { state.send('MOVED', {
...info, ...info,
type: MoveType.Backward, type: MoveType.Backward,
@ -136,7 +218,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'Backspace': { case 'Backspace': {
if (metaKey(e)) { if (meta) {
if (e.shiftKey) { if (e.shiftKey) {
if (window.confirm('Reset document and state?')) { if (window.confirm('Reset document and state?')) {
state.send('RESET_DOCUMENT_STATE', info) state.send('RESET_DOCUMENT_STATE', info)
@ -150,7 +232,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'g': { case 'g': {
if (metaKey(e)) { if (meta) {
if (e.shiftKey) { if (e.shiftKey) {
state.send('UNGROUPED', info) state.send('UNGROUPED', info)
} else { } else {
@ -160,7 +242,7 @@ export default function useKeyboardEvents() {
break break
} }
case 's': { case 's': {
if (metaKey(e)) { if (meta) {
if (e.shiftKey) { if (e.shiftKey) {
state.send('SAVED_AS_TO_FILESYSTEM', info) state.send('SAVED_AS_TO_FILESYSTEM', info)
} else { } else {
@ -170,7 +252,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'o': { case 'o': {
if (metaKey(e)) { if (meta) {
break break
} else { } else {
state.send('SELECTED_DOT_TOOL', info) state.send('SELECTED_DOT_TOOL', info)
@ -178,7 +260,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'v': { case 'v': {
if (metaKey(e)) { if (meta) {
state.send('PASTED', info) state.send('PASTED', info)
} else { } else {
state.send('SELECTED_SELECT_TOOL', info) state.send('SELECTED_SELECT_TOOL', info)
@ -186,7 +268,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'a': { case 'a': {
if (metaKey(e)) { if (meta) {
state.send('SELECTED_ALL', info) state.send('SELECTED_ALL', info)
} else { } else {
state.send('SELECTED_ARROW_TOOL', info) state.send('SELECTED_ARROW_TOOL', info)
@ -194,7 +276,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'd': { case 'd': {
if (metaKey(e)) { if (meta) {
state.send('DUPLICATED', info) state.send('DUPLICATED', info)
} else { } else {
state.send('SELECTED_DRAW_TOOL', info) state.send('SELECTED_DRAW_TOOL', info)
@ -206,7 +288,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'c': { case 'c': {
if (metaKey(e)) { if (meta) {
if (e.shiftKey) { if (e.shiftKey) {
state.send('COPIED_TO_SVG', info) state.send('COPIED_TO_SVG', info)
} else { } else {
@ -218,7 +300,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'i': { case 'i': {
if (metaKey(e)) { if (meta) {
break break
} else { } else {
state.send('SELECTED_CIRCLE_TOOL', info) state.send('SELECTED_CIRCLE_TOOL', info)
@ -226,7 +308,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'l': { case 'l': {
if (metaKey(e)) { if (meta) {
if (e.shiftKey) { if (e.shiftKey) {
state.send('TOGGLED_LOGGER') state.send('TOGGLED_LOGGER')
} else { } else {
@ -238,7 +320,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'y': { case 'y': {
if (metaKey(e)) { if (meta) {
break break
} else { } else {
state.send('SELECTED_RAY_TOOL', info) state.send('SELECTED_RAY_TOOL', info)
@ -246,7 +328,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'p': { case 'p': {
if (metaKey(e)) { if (meta) {
break break
} else { } else {
state.send('SELECTED_POLYLINE_TOOL', info) state.send('SELECTED_POLYLINE_TOOL', info)
@ -254,7 +336,7 @@ export default function useKeyboardEvents() {
break break
} }
case 'r': { case 'r': {
if (metaKey(e)) { if (meta) {
break break
} else { } else {
state.send('SELECTED_RECTANGLE_TOOL', info) state.send('SELECTED_RECTANGLE_TOOL', info)

View file

@ -6,10 +6,10 @@ const canvasLight = '#fafafa'
const canvasDark = '#343d45' const canvasDark = '#343d45'
const colors = { const colors = {
[ColorStyle.Black]: '#212528',
[ColorStyle.White]: '#f0f1f3', [ColorStyle.White]: '#f0f1f3',
[ColorStyle.LightGray]: '#c6cbd1', [ColorStyle.LightGray]: '#c6cbd1',
[ColorStyle.Gray]: '#788492', [ColorStyle.Gray]: '#788492',
[ColorStyle.Black]: '#212528',
[ColorStyle.Green]: '#36b24d', [ColorStyle.Green]: '#36b24d',
[ColorStyle.Cyan]: '#0e98ad', [ColorStyle.Cyan]: '#0e98ad',
[ColorStyle.Blue]: '#1c7ed6', [ColorStyle.Blue]: '#1c7ed6',

View file

@ -108,7 +108,7 @@ const dark = theme({
border: '#202529', border: '#202529',
canvas: '#343d45', canvas: '#343d45',
panel: '#49555f', panel: '#49555f',
inactive: '#cccccf', inactive: '#aaaaad',
hover: '#343d45', hover: '#343d45',
text: '#f8f9fa', text: '#f8f9fa',
muted: '#e0e2e6', muted: '#e0e2e6',

View file

@ -1586,17 +1586,21 @@ export function getFromCache<V, I extends object>(
return value return value
} }
const byteToHex = []
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).substr(1))
}
/** /**
* Get a unique string id. * Get a unique string id.
*/ */
export function uniqueId(): string {
const array = new Uint32Array(8) export function uniqueId(a = ''): string {
window.crypto.getRandomValues(array) return a
let str = '' ? /* eslint-disable no-bitwise */
for (let i = 0; i < array.length; i++) { ((Number(a) ^ (Math.random() * 16)) >> (Number(a) / 4)).toString(16)
str += (i < 2 || i > 5 ? '' : '-') + array[i].toString(16).slice(-4) : `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`.replace(/[018]/g, uniqueId)
}
return str
} }
/** /**