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',
cursor: 'pointer',
'&:focus': {
backgroundColor: '$hover',
},
'&:hover:not(:disabled)': {
backgroundColor: '$hover',
'& svg': {
stroke: '$text',
fill: '$text',
strokeWidth: '0',
},
},
'&: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', {
height: '100%',
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 { breakpoints, IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import { fills, strokes } from 'state/shape-styles'
import { useSelector } from 'state'
import ColorContent from './color-content'
import { BoxIcon } from '../shared'
import { strokes } from 'state/shape-styles'
import state, { useSelector } from 'state'
import { BoxIcon, Item, DropdownContent } from '../shared'
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 {
const color = useSelector((s) => s.values.selectedStyle.color)
@ -15,10 +19,33 @@ export default function QuickColorSelect(): JSX.Element {
<DropdownMenu.Root dir="ltr">
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
<Tooltip label="Color">
<BoxIcon fill={fills[theme][color]} stroke={strokes[theme][color]} />
<BoxIcon
fill={strokes[theme][color]}
stroke={strokes[theme][color]}
/>
</Tooltip>
</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>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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