Merge pull request #48 from tldraw/tool-keyboard-shortcuts
Tool keyboard shortcuts
This commit is contained in:
commit
77403941f8
9 changed files with 190 additions and 99 deletions
|
@ -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',
|
||||
|
|
|
@ -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)
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -108,7 +108,7 @@ const dark = theme({
|
|||
border: '#202529',
|
||||
canvas: '#343d45',
|
||||
panel: '#49555f',
|
||||
inactive: '#cccccf',
|
||||
inactive: '#aaaaad',
|
||||
hover: '#343d45',
|
||||
text: '#f8f9fa',
|
||||
muted: '#e0e2e6',
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue