Add IDs to UI components (#511)

* Add IDs to Menu and its sub-menus

* Commit uncommitted

* Added ID on Styles and Zoom menu

* Added ID on Tools menu

* Added ID on Context menu
This commit is contained in:
Faraz Shaikh 2022-01-21 15:44:18 +04:00 committed by GitHub
parent 5b521df51e
commit dd1fb73876
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 387 additions and 186 deletions

View file

@ -163,44 +163,48 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp
tabIndex={-1}
onBlur={onBlur}
>
<MenuContent>
<MenuContent id="TD-ContextMenu">
{hasSelection ? (
<>
<CMRowButton onClick={handleDuplicate} kbd="#D">
<CMRowButton onClick={handleDuplicate} kbd="#D" id="TD-ContextMenu-Duplicate">
Duplicate
</CMRowButton>
<CMRowButton onClick={handleFlipHorizontal} kbd="⇧H">
<CMRowButton
onClick={handleFlipHorizontal}
kbd="⇧H"
id="TD-ContextMenu-Flip_Horizontal"
>
Flip Horizontal
</CMRowButton>
<CMRowButton onClick={handleFlipVertical} kbd="⇧V">
<CMRowButton onClick={handleFlipVertical} kbd="⇧V" id="TD-ContextMenu-Flip_Vertical">
Flip Vertical
</CMRowButton>
<CMRowButton onClick={handleLock} kbd="#⇧L">
<CMRowButton onClick={handleLock} kbd="#⇧L" id="TD-ContextMenu- Lock_Unlock">
Lock / Unlock
</CMRowButton>
{(hasTwoOrMore || hasGroupSelected) && <Divider />}
{hasTwoOrMore && (
<CMRowButton onClick={handleGroup} kbd="#G">
<CMRowButton onClick={handleGroup} kbd="#G" id="TD-ContextMenu-Group">
Group
</CMRowButton>
)}
{hasGroupSelected && (
<CMRowButton onClick={handleGroup} kbd="#G">
<CMRowButton onClick={handleGroup} kbd="#G" id="TD-ContextMenu-Ungroup">
Ungroup
</CMRowButton>
)}
<Divider />
<ContextMenuSubMenu label="Move">
<CMRowButton onClick={handleMoveToFront} kbd="⇧]">
<ContextMenuSubMenu label="Move" id="TD-ContextMenu-Move">
<CMRowButton onClick={handleMoveToFront} kbd="⇧]" id="TD-ContextMenu-Move-To_Front">
To Front
</CMRowButton>
<CMRowButton onClick={handleMoveForward} kbd="]">
<CMRowButton onClick={handleMoveForward} kbd="]" id="TD-ContextMenu-Move-Forward">
Forward
</CMRowButton>
<CMRowButton onClick={handleMoveBackward} kbd="[">
<CMRowButton onClick={handleMoveBackward} kbd="[" id="TD-ContextMenu-Move-Backward">
Backward
</CMRowButton>
<CMRowButton onClick={handleMoveToBack} kbd="⇧[">
<CMRowButton onClick={handleMoveToBack} kbd="⇧[" id="TD-ContextMenu-Move-To_Back">
To Back
</CMRowButton>
</ContextMenuSubMenu>
@ -211,53 +215,79 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp
{app.callbacks.onExport ? (
<>
<Divider />
<ContextMenuSubMenu label="Export" size="small">
<CMRowButton onClick={handleExportPNG}>PNG</CMRowButton>
<CMRowButton onClick={handleExportJPG}>JPG</CMRowButton>
<CMRowButton onClick={handleExportWEBP}>WEBP</CMRowButton>
<CMRowButton onClick={handleExportSVG}>SVG</CMRowButton>
<CMRowButton onClick={handleExportJSON}>JSON</CMRowButton>
<ContextMenuSubMenu label="Export" size="small" id="TD-ContextMenu-Export">
<CMRowButton onClick={handleExportPNG} id="TD-ContextMenu-Export-PNG">
PNG
</CMRowButton>
<CMRowButton onClick={handleExportJPG} id="TD-ContextMenu-Export-JPG">
JPG
</CMRowButton>
<CMRowButton onClick={handleExportWEBP} id="TD-ContextMenu-Export-WEBP">
WEBP
</CMRowButton>
<CMRowButton onClick={handleExportSVG} id="TD-ContextMenu-Export-SVG">
SVG
</CMRowButton>
<CMRowButton onClick={handleExportJSON} id="TD-ContextMenu-Export-JSON">
JSON
</CMRowButton>
<Divider />
<CMRowButton onClick={handleCopySvg} kbd="#⇧C">
<CMRowButton
onClick={handleCopySvg}
kbd="#⇧C"
id="TD-ContextMenu-Export-Copy_as_SVG"
>
Copy as SVG
</CMRowButton>
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
{isDebugMode && (
<CMRowButton onClick={handleCopyJson} id="TD-ContextMenu-Export-Copy_as_JSON">
Copy as JSON
</CMRowButton>
)}
</ContextMenuSubMenu>
</>
) : (
<>
<Divider />
<CMRowButton onClick={handleCopySvg} kbd="#⇧C">
<CMRowButton
onClick={handleCopySvg}
kbd="#⇧C"
id="TD-ContextMenu-Export-Copy_as_SVG"
>
Copy as SVG
</CMRowButton>
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
{isDebugMode && (
<CMRowButton onClick={handleCopyJson} id="TD-ContextMenu-Export-Copy_as_JSON">
Copy as JSON
</CMRowButton>
)}
</>
)}
<Divider />
<CMRowButton onClick={handleCut} kbd="#X">
<CMRowButton onClick={handleCut} kbd="#X" id="TD-ContextMenu-Cut">
Cut
</CMRowButton>
<CMRowButton onClick={handleCopy} kbd="#C">
<CMRowButton onClick={handleCopy} kbd="#C" id="TD-ContextMenu-Copy">
Copy
</CMRowButton>
<CMRowButton onClick={handlePaste} kbd="#V">
<CMRowButton onClick={handlePaste} kbd="#V" id="TD-ContextMenu-Paste">
Paste
</CMRowButton>
<Divider />
<CMRowButton onClick={handleDelete} kbd="⌫">
<CMRowButton onClick={handleDelete} kbd="⌫" id="TD-ContextMenu-Delete">
Delete
</CMRowButton>
</>
) : (
<>
<CMRowButton onClick={handlePaste} kbd="#V">
<CMRowButton onClick={handlePaste} kbd="#V" id="TD-ContextMenu-Paste">
Paste
</CMRowButton>
<CMRowButton onClick={handleUndo} kbd="#Z">
<CMRowButton onClick={handleUndo} kbd="#Z" id="TD-ContextMenu-Undo">
Undo
</CMRowButton>
<CMRowButton onClick={handleRedo} kbd="#⇧Z">
<CMRowButton onClick={handleRedo} kbd="#⇧Z" id="TD-ContextMenu-Redo">
Redo
</CMRowButton>
</>
@ -318,48 +348,68 @@ function AlignDistributeSubMenu({
}, [app])
return (
<RadixContextMenu.Root dir="ltr">
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
<StyledGridContent numberOfSelected={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
<CMIconButton onClick={alignLeft}>
<AlignLeftIcon />
</CMIconButton>
<CMIconButton onClick={alignCenterHorizontal}>
<AlignCenterHorizontallyIcon />
</CMIconButton>
<CMIconButton onClick={alignRight}>
<AlignRightIcon />
</CMIconButton>
<CMIconButton onClick={stretchHorizontally}>
<StretchHorizontallyIcon />
</CMIconButton>
{hasThreeOrMore && (
<CMIconButton onClick={distributeHorizontally}>
<SpaceEvenlyHorizontallyIcon />
<span id="TD-ContextMenu-Align_Duplicate">
<RadixContextMenu.Root dir="ltr">
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
<StyledGridContent numberOfSelected={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
<CMIconButton onClick={alignLeft} id="TD-ContextMenu-Align_Duplicate-AlignLeft">
<AlignLeftIcon />
</CMIconButton>
)}
<CMIconButton onClick={alignTop}>
<AlignTopIcon />
</CMIconButton>
<CMIconButton onClick={alignCenterVertical}>
<AlignCenterVerticallyIcon />
</CMIconButton>
<CMIconButton onClick={alignBottom}>
<AlignBottomIcon />
</CMIconButton>
<CMIconButton onClick={stretchVertically}>
<StretchVerticallyIcon />
</CMIconButton>
{hasThreeOrMore && (
<CMIconButton onClick={distributeVertically}>
<SpaceEvenlyVerticallyIcon />
<CMIconButton
onClick={alignCenterHorizontal}
id="TD-ContextMenu-Align_Duplicate-AlignCenterHorizontal"
>
<AlignCenterHorizontallyIcon />
</CMIconButton>
)}
<CMArrow offset={13} />
</StyledGridContent>
</RadixContextMenu.Content>
</RadixContextMenu.Root>
<CMIconButton onClick={alignRight} id="TD-ContextMenu-Align_Duplicate-AlignRight">
<AlignRightIcon />
</CMIconButton>
<CMIconButton
onClick={stretchHorizontally}
id="TD-ContextMenu-Align_Duplicate-StretchHorizontal"
>
<StretchHorizontallyIcon />
</CMIconButton>
{hasThreeOrMore && (
<CMIconButton
onClick={distributeHorizontally}
id="TD-ContextMenu-Align_Duplicate-SpaceEvenlyHorizontal"
>
<SpaceEvenlyHorizontallyIcon />
</CMIconButton>
)}
<CMIconButton onClick={alignTop} id="TD-ContextMenu-Align_Duplicate-AlignTop">
<AlignTopIcon />
</CMIconButton>
<CMIconButton
onClick={alignCenterVertical}
id="TD-ContextMenu-Align_Duplicate-AlignCenterVertical"
>
<AlignCenterVerticallyIcon />
</CMIconButton>
<CMIconButton onClick={alignBottom} id="TD-ContextMenu-Align_Duplicate-AlignBottom">
<AlignBottomIcon />
</CMIconButton>
<CMIconButton
onClick={stretchVertically}
id="TD-ContextMenu-Align_Duplicate-StretchVertical"
>
<StretchVerticallyIcon />
</CMIconButton>
{hasThreeOrMore && (
<CMIconButton
onClick={distributeVertically}
id="TD-ContextMenu-Align_Duplicate-SpaceEvenlyVertical"
>
<SpaceEvenlyVerticallyIcon />
</CMIconButton>
)}
<CMArrow offset={13} />
</StyledGridContent>
</RadixContextMenu.Content>
</RadixContextMenu.Root>
</span>
)
}
@ -420,23 +470,27 @@ export interface ContextMenuSubMenuProps {
label: string
size?: 'small'
children: React.ReactNode
id?: string
}
export function ContextMenuSubMenu({
children,
label,
size,
id,
}: ContextMenuSubMenuProps): JSX.Element {
return (
<RadixContextMenu.Root dir="ltr">
<CMTriggerButton isSubmenu>{label}</CMTriggerButton>
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
<MenuContent size={size}>
{children}
<CMArrow offset={13} />
</MenuContent>
</RadixContextMenu.Content>
</RadixContextMenu.Root>
<span id={id}>
<RadixContextMenu.Root dir="ltr">
<CMTriggerButton isSubmenu>{label}</CMTriggerButton>
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
<MenuContent size={size}>
{children}
<CMArrow offset={13} />
</MenuContent>
</RadixContextMenu.Content>
</RadixContextMenu.Root>
</span>
)
}
@ -458,9 +512,9 @@ function CMIconButton({ onSelect, ...rest }: ToolButtonProps): JSX.Element {
/* -------------------- RowButton ------------------- */
const CMRowButton = ({ ...rest }: RowButtonProps) => {
const CMRowButton = ({ id, ...rest }: RowButtonProps) => {
return (
<RadixContextMenu.ContextMenuItem asChild>
<RadixContextMenu.ContextMenuItem asChild id={id}>
<RowButton {...rest} />
</RadixContextMenu.ContextMenuItem>
)

View file

@ -10,6 +10,7 @@ interface DMCheckboxItemProps {
children: React.ReactNode
variant?: RowButtonProps['variant']
kbd?: string
id?: string
}
export function DMCheckboxItem({
@ -18,6 +19,7 @@ export function DMCheckboxItem({
variant,
onCheckedChange,
kbd,
id,
children,
}: DMCheckboxItemProps): JSX.Element {
return (
@ -28,6 +30,7 @@ export function DMCheckboxItem({
checked={checked}
disabled={disabled}
asChild
id={id}
>
<RowButton kbd={kbd} variant={variant} hasIndicator>
{children}

View file

@ -9,6 +9,7 @@ export interface DMContentProps {
align?: 'start' | 'center' | 'end'
sideOffset?: number
children: React.ReactNode
id?: string
}
export function DMContent({
@ -16,6 +17,7 @@ export function DMContent({
children,
align,
variant,
id,
}: DMContentProps): JSX.Element {
return (
<Content
@ -24,6 +26,7 @@ export function DMContent({
sideOffset={sideOffset}
onEscapeKeyDown={stopPropagation}
asChild
id={id}
>
<StyledContent variant={variant}>{children}</StyledContent>
</Content>

View file

@ -4,10 +4,11 @@ import { RowButton, RowButtonProps } from '~components/Primitives/RowButton'
export function DMItem({
onSelect,
id,
...rest
}: RowButtonProps & { onSelect?: (event: Event) => void }): JSX.Element {
}: RowButtonProps & { onSelect?: (event: Event) => void; id?: string }): JSX.Element {
return (
<Item dir="ltr" asChild onSelect={onSelect}>
<Item dir="ltr" asChild onSelect={onSelect} id={id}>
<RowButton {...rest} />
</Item>
)

View file

@ -8,6 +8,7 @@ export interface DMSubMenuProps {
size?: 'small'
disabled?: boolean
children: React.ReactNode
id?: string
}
export function DMSubMenu({
@ -15,20 +16,23 @@ export function DMSubMenu({
size,
disabled = false,
label,
id,
}: DMSubMenuProps): JSX.Element {
return (
<Root dir="ltr">
<TriggerItem dir="ltr" asChild>
<RowButton disabled={disabled} hasArrow>
{label}
</RowButton>
</TriggerItem>
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2}>
<MenuContent size={size}>
{children}
<Arrow offset={13} />
</MenuContent>
</Content>
</Root>
<span id={id}>
<Root dir="ltr">
<TriggerItem dir="ltr" asChild>
<RowButton disabled={disabled} hasArrow>
{label}
</RowButton>
</TriggerItem>
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2}>
<MenuContent size={size}>
{children}
<Arrow offset={13} />
</MenuContent>
</Content>
</Root>
</span>
)
}

View file

@ -4,11 +4,12 @@ import { ToolButton, ToolButtonProps } from '~components/Primitives/ToolButton'
interface DMTriggerIconProps extends ToolButtonProps {
children: React.ReactNode
id?: string
}
export function DMTriggerIcon({ children, ...rest }: DMTriggerIconProps) {
export function DMTriggerIcon({ id, children, ...rest }: DMTriggerIconProps) {
return (
<Trigger asChild>
<Trigger asChild id={id}>
<ToolButton {...rest}>{children}</ToolButton>
</Trigger>
)

View file

@ -17,6 +17,7 @@ export interface RowButtonProps {
isWarning?: boolean
hasIndicator?: boolean
hasArrow?: boolean
id?: string
}
export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(

View file

@ -14,6 +14,7 @@ export interface ToolButtonProps {
isToolLocked?: boolean
variant?: 'icon' | 'text' | 'circle' | 'primary'
children: React.ReactNode
id?: string
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>
}
@ -30,6 +31,7 @@ export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
isActive = false,
isSponsor = false,
onKeyDown,
id,
...rest
},
ref
@ -46,6 +48,7 @@ export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
onDoubleClick={onDoubleClick}
onKeyDown={onKeyDown}
bp={breakpoints}
id={id}
{...rest}
>
<StyledToolButtonInner>{children}</StyledToolButtonInner>

View file

@ -11,6 +11,7 @@ interface TooltipProps {
children: React.ReactNode
label: string
kbd?: string
id?: string
side?: 'bottom' | 'left' | 'right' | 'top'
}
@ -18,19 +19,22 @@ export function Tooltip({
children,
label,
kbd: kbdProp,
id,
side = 'top',
}: TooltipProps): JSX.Element {
return (
<RadixTooltip.Root>
<RadixTooltip.Trigger dir="ltr" asChild={true}>
<span>{children}</span>
</RadixTooltip.Trigger>
<StyledContent dir="ltr" side={side} sideOffset={8}>
{label}
{kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
<StyledArrow />
</StyledContent>
</RadixTooltip.Root>
<span id={id}>
<RadixTooltip.Root>
<RadixTooltip.Trigger dir="ltr" asChild={true}>
<span>{children}</span>
</RadixTooltip.Trigger>
<StyledContent dir="ltr" side={side} sideOffset={8}>
{label}
{kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
<StyledArrow />
</StyledContent>
</RadixTooltip.Root>
</span>
)
}

View file

@ -180,7 +180,7 @@ export function ActionButton(): JSX.Element {
return (
<DropdownMenu.Root dir="ltr" onOpenChange={handleMenuOpenChange}>
<DropdownMenu.Trigger dir="ltr" asChild>
<DropdownMenu.Trigger dir="ltr" asChild id="TD-Tools-Dots">
<ToolButton variant="circle">
<DotsHorizontalIcon />
</ToolButton>
@ -189,22 +189,22 @@ export function ActionButton(): JSX.Element {
<>
<ButtonsRow>
<ToolButton variant="icon" disabled={!hasSelection} onClick={handleDuplicate}>
<Tooltip label="Duplicate" kbd={`#D`}>
<Tooltip label="Duplicate" kbd={`#D`} id="TD-Tools-Copy">
<CopyIcon />
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleRotate}>
<Tooltip label="Rotate">
<Tooltip label="Rotate" id="TD-Tools-Rotate">
<RotateCounterClockwiseIcon />
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleToggleLocked}>
<Tooltip label="Toggle Locked" kbd={`#L`}>
<Tooltip label="Toggle Locked" kbd={`#L`} id="TD-Tools-Lock">
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleToggleAspectRatio}>
<Tooltip label="Toggle Aspect Ratio Lock">
<Tooltip label="Toggle Aspect Ratio Lock" id="TD-Tools-AspectRatio">
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
</Tooltip>
</ToolButton>
@ -212,70 +212,94 @@ export function ActionButton(): JSX.Element {
disabled={!hasSelection || (!isAllGrouped && !hasMultipleSelection)}
onClick={handleGroup}
>
<Tooltip label="Group" kbd={`#G`}>
<Tooltip label="Group" kbd={`#G`} id="TD-Tools-Group">
<GroupIcon />
</Tooltip>
</ToolButton>
</ButtonsRow>
<ButtonsRow>
<ToolButton disabled={!hasSelection} onClick={handleMoveToBack}>
<Tooltip label="Move to Back" kbd={`#⇧[`}>
<Tooltip label="Move to Back" kbd={`#⇧[`} id="TD-Tools-PinBottom">
<PinBottomIcon />
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleMoveBackward}>
<Tooltip label="Move Backward" kbd={`#[`}>
<Tooltip label="Move Backward" kbd={`#[`} id="TD-Tools-ArrowDown">
<ArrowDownIcon />
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleMoveForward}>
<Tooltip label="Move Forward" kbd={`#]`}>
<Tooltip label="Move Forward" kbd={`#]`} id="TD-Tools-ArrowUp">
<ArrowUpIcon />
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleMoveToFront}>
<Tooltip label="Move to Front" kbd={`#⇧]`}>
<Tooltip label="Move to Front" kbd={`#⇧]`} id="TD-Tools-PinTop">
<PinTopIcon />
</Tooltip>
</ToolButton>
<ToolButton disabled={!hasSelection} onClick={handleResetAngle}>
<Tooltip label="Reset Angle">
<Tooltip label="Reset Angle" id="TD-Tools-ResetAngle">
<AngleIcon />
</Tooltip>
</ToolButton>
</ButtonsRow>
<Divider />
<ButtonsRow>
<ToolButton disabled={!hasTwoOrMore} onClick={alignLeft}>
<ToolButton disabled={!hasTwoOrMore} onClick={alignLeft} id="TD-Tools-AlignLeft">
<AlignLeftIcon />
</ToolButton>
<ToolButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
<ToolButton
disabled={!hasTwoOrMore}
onClick={alignCenterHorizontal}
id="TD-Tools-AlignCenterHorizontal"
>
<AlignCenterHorizontallyIcon />
</ToolButton>
<ToolButton disabled={!hasTwoOrMore} onClick={alignRight}>
<ToolButton disabled={!hasTwoOrMore} onClick={alignRight} id="TD-Tools-AlignRight">
<AlignRightIcon />
</ToolButton>
<ToolButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
<ToolButton
disabled={!hasTwoOrMore}
onClick={stretchHorizontally}
id="TD-Tools-StretchHorizontal"
>
<StretchHorizontallyIcon />
</ToolButton>
<ToolButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
<ToolButton
disabled={!hasThreeOrMore}
onClick={distributeHorizontally}
id="TD-Tools-SpaceEvenlyHorizontal"
>
<SpaceEvenlyHorizontallyIcon />
</ToolButton>
</ButtonsRow>
<ButtonsRow>
<ToolButton disabled={!hasTwoOrMore} onClick={alignTop}>
<ToolButton disabled={!hasTwoOrMore} onClick={alignTop} id="TD-Tools-AlignTop">
<AlignTopIcon />
</ToolButton>
<ToolButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
<ToolButton
disabled={!hasTwoOrMore}
onClick={alignCenterVertical}
id="TD-Tools-AlignCenterVertical"
>
<AlignCenterVerticallyIcon />
</ToolButton>
<ToolButton disabled={!hasTwoOrMore} onClick={alignBottom}>
<ToolButton disabled={!hasTwoOrMore} onClick={alignBottom} id="TD-Tools-AlignBottom">
<AlignBottomIcon />
</ToolButton>
<ToolButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
<ToolButton
disabled={!hasTwoOrMore}
onClick={stretchVertically}
id="TD-Tools-tretchVertical"
>
<StretchVerticallyIcon />
</ToolButton>
<ToolButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
<ToolButton
disabled={!hasThreeOrMore}
onClick={distributeVertically}
id="TD-Tools-SpaceEvenlyVertical"
>
<SpaceEvenlyVerticallyIcon />
</ToolButton>
</ButtonsRow>

View file

@ -17,7 +17,7 @@ export const BackToContent = React.memo(function BackToContent() {
if (!isEmptyCanvas) return null
return (
<BackToContentContainer>
<BackToContentContainer id="TD-Tools-Back_to_content">
<RowButton onClick={app.zoomToContent}>Back to content</RowButton>
</BackToContentContainer>
)

View file

@ -18,7 +18,7 @@ export function DeleteButton(): JSX.Element {
)
return (
<Tooltip label="Delete" kbd="⌫">
<Tooltip label="Delete" kbd="⌫" id="TD-Delete">
<ToolButton variant="circle" disabled={!hasSelection} onSelect={handleDelete}>
<TrashIcon />
</ToolButton>

View file

@ -13,7 +13,7 @@ export function LockButton(): JSX.Element {
const isToolLocked = app.useStore(isToolLockedSelector)
return (
<Tooltip label="Lock Tool" kbd="7">
<Tooltip label="Lock Tool" kbd="7" id="TD-Lock">
<ToolButton variant="circle" isActive={isToolLocked} onSelect={app.toggleToolLock}>
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</ToolButton>

View file

@ -44,6 +44,7 @@ export const PenMenu = React.memo(function PenMenu({ activeTool }: ShapesMenuPro
onDoubleClick={handleDoubleClick}
onClick={selectShapeTool}
isActive={penShapes.includes(activeTool as PenShape)}
id="TD-Pen"
>
{penShapeIcons[lastActiveTool]}
</ToolButton>
@ -55,6 +56,7 @@ export const PenMenu = React.memo(function PenMenu({ activeTool }: ShapesMenuPro
key={shape}
label={shape[0].toUpperCase() + shape.slice(1)}
kbd={(1 + i).toString()}
id={`TD-Pen-${shape}`}
>
<DropdownMenu.Item asChild>
<ToolButton

View file

@ -48,12 +48,13 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
}, [app])
return (
<Panel side="center">
<Panel side="center" id="TD-PrimaryTools">
<ToolButtonWithTooltip
kbd={'1'}
label={'select'}
onClick={selectSelectTool}
isActive={activeTool === 'select'}
id="TD-PrimaryTools-CursorArrow"
>
<CursorArrowIcon />
</ToolButtonWithTooltip>
@ -62,6 +63,7 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
label={TDShapeType.Draw}
onClick={selectDrawTool}
isActive={activeTool === TDShapeType.Draw}
id="TD-PrimaryTools-Pencil"
>
<Pencil1Icon />
</ToolButtonWithTooltip>
@ -70,6 +72,7 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
label={'eraser'}
onClick={selectEraseTool}
isActive={activeTool === 'erase'}
id="TD-PrimaryTools-Eraser"
>
<EraserIcon />
</ToolButtonWithTooltip>
@ -80,6 +83,7 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
onClick={selectArrowTool}
isLocked={isToolLocked}
isActive={activeTool === TDShapeType.Arrow}
id="TD-PrimaryTools-ArrowTopRight"
>
<ArrowTopRightIcon />
</ToolButtonWithTooltip>
@ -89,6 +93,7 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
onClick={selectTextTool}
isLocked={isToolLocked}
isActive={activeTool === TDShapeType.Text}
id="TD-PrimaryTools-Text"
>
<TextIcon />
</ToolButtonWithTooltip>
@ -97,6 +102,7 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
label={TDShapeType.Sticky}
onClick={selectStickyTool}
isActive={activeTool === TDShapeType.Sticky}
id="TD-PrimaryTools-Pencil2"
>
<Pencil2Icon />
</ToolButtonWithTooltip>

View file

@ -75,7 +75,7 @@ export const ShapesMenu = React.memo(function ShapesMenu({
return (
<DropdownMenu.Root dir="ltr" onOpenChange={selectShapeTool}>
<DropdownMenu.Trigger dir="ltr" asChild>
<DropdownMenu.Trigger dir="ltr" asChild id="TD-PrimaryTools-Shapes">
<ToolButton
disabled={isActive && app.shiftKey} // otherwise this continuously opens and closes on "SpacePanning"
variant="primary"
@ -94,6 +94,7 @@ export const ShapesMenu = React.memo(function ShapesMenu({
key={shape}
label={shape[0].toUpperCase() + shape.slice(1)}
kbd={(4 + i).toString()}
id={`TD-PrimaryTools-Shapes-${shape}`}
>
<DropdownMenu.Item asChild>
<ToolButton

View file

@ -13,7 +13,7 @@ export function StatusBar(): JSX.Element | null {
const activeTool = app.useStore(activeToolSelector)
return (
<StyledStatusBar bp={breakpoints}>
<StyledStatusBar bp={breakpoints} id="TD-StatusBar">
<StyledSection>
{activeTool} | {status}
</StyledSection>

View file

@ -20,7 +20,7 @@ export const ToolsPanel = React.memo(function ToolsPanel({ onBlur }: ToolsPanelP
return (
<StyledToolsPanelContainer onBlur={onBlur}>
<StyledCenterWrap>
<StyledCenterWrap id="TD-Tools">
<BackToContent />
<StyledPrimaryTools>
<ActionButton />

View file

@ -121,48 +121,58 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
return (
<DropdownMenu.Root dir="ltr">
<DMTriggerIcon isSponsor={showSponsorLink}>
<DMTriggerIcon isSponsor={showSponsorLink} id="TD-MenuIcon">
<HamburgerMenuIcon />
</DMTriggerIcon>
<DMContent variant="menu">
<DMContent variant="menu" id="TD-Menu">
{showFileMenu && (
<DMSubMenu label="File...">
<DMSubMenu label="File..." id="TD-MenuItem-File">
{app.callbacks.onNewProject && (
<DMItem onClick={onNewProject} kbd="#N">
<DMItem onClick={onNewProject} kbd="#N" id="TD-MenuItem-File-New_Project">
New Project
</DMItem>
)}
{app.callbacks.onOpenProject && (
<DMItem onClick={onOpenProject} kbd="#O">
<DMItem onClick={onOpenProject} kbd="#O" id="TD-MenuItem-File-Open">
Open...
</DMItem>
)}
{app.callbacks.onSaveProject && (
<DMItem onClick={onSaveProject} kbd="#S">
<DMItem onClick={onSaveProject} kbd="#S" id="TD-MenuItem-File-Save">
Save
</DMItem>
)}
{app.callbacks.onSaveProjectAs && (
<DMItem onClick={onSaveProjectAs} kbd="#⇧S">
<DMItem onClick={onSaveProjectAs} kbd="#⇧S" id="TD-MenuItem-File-Save_As">
Save As...
</DMItem>
)}
{app.callbacks.onExport && (
<>
<Divider />
<DMSubMenu label="Export" size="small">
<DMItem onClick={handleExportPNG}>PNG</DMItem>
<DMItem onClick={handleExportJPG}>JPG</DMItem>
<DMItem onClick={handleExportWEBP}>WEBP</DMItem>
<DMItem onClick={handleExportSVG}>SVG</DMItem>
<DMItem onClick={handleExportJSON}>JSON</DMItem>
<DMSubMenu label="Export" size="small" id="TD-MenuItem-File-Export">
<DMItem onClick={handleExportPNG} id="TD-MenuItem-File-Export-PNG">
PNG
</DMItem>
<DMItem onClick={handleExportJPG} id="TD-MenuItem-File-Export-JPG">
JPG
</DMItem>
<DMItem onClick={handleExportWEBP} id="TD-MenuItem-File-Export-WEBP">
WEBP
</DMItem>
<DMItem onClick={handleExportSVG} id="TD-MenuItem-File-Export-SVG">
SVG
</DMItem>
<DMItem onClick={handleExportJSON} id="TD-MenuItem-File-Export-JSON">
JSON
</DMItem>
</DMSubMenu>
</>
)}
{!disableAssets && (
<>
<Divider />
<DMItem onClick={handleUploadMedia} kbd="#U">
<DMItem onClick={handleUploadMedia} kbd="#U" id="TD-MenuItem-File-Upload_Media">
Upload Media
</DMItem>
</>
@ -171,15 +181,31 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
)}
{!readOnly && (
<>
<DMSubMenu label="Edit...">
<DMItem onSelect={preventEvent} onClick={app.undo} kbd="#Z">
<DMSubMenu label="Edit..." id="TD-MenuItem-Edit">
<DMItem
onSelect={preventEvent}
onClick={app.undo}
kbd="#Z"
id="TD-MenuItem-Edit-Undo"
>
Undo
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.redo} kbd="#⇧Z">
<DMItem
onSelect={preventEvent}
onClick={app.redo}
kbd="#⇧Z"
id="TD-MenuItem-Edit-Redo"
>
Redo
</DMItem>
<DMDivider dir="ltr" />
<DMItem onSelect={preventEvent} disabled={!hasSelection} onClick={handleCut} kbd="#X">
<DMItem
onSelect={preventEvent}
disabled={!hasSelection}
onClick={handleCut}
kbd="#X"
id="TD-MenuItem-Edit-Cut"
>
Cut
</DMItem>
<DMItem
@ -187,10 +213,16 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
disabled={!hasSelection}
onClick={handleCopy}
kbd="#C"
id="TD-MenuItem-Edit-Copy"
>
Copy
</DMItem>
<DMItem onSelect={preventEvent} onClick={handlePaste} kbd="#V">
<DMItem
onSelect={preventEvent}
onClick={handlePaste}
kbd="#V"
id="TD-MenuItem-Edit-Paste"
>
Paste
</DMItem>
<DMDivider dir="ltr" />
@ -199,30 +231,45 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
disabled={!hasSelection}
onClick={handleCopySvg}
kbd="#⇧C"
id="TD-MenuItem-Edit-Copy_as_SVG"
>
Copy as SVG
</DMItem>
<DMItem onSelect={preventEvent} disabled={!hasSelection} onClick={handleCopyJson}>
<DMItem
onSelect={preventEvent}
disabled={!hasSelection}
onClick={handleCopyJson}
id="TD-MenuItem-Edit-Copy_as_JSON"
>
Copy as JSON
</DMItem>
<DMDivider dir="ltr" />
<DMItem onSelect={preventEvent} onClick={handleSelectAll} kbd="#A">
<DMItem
onSelect={preventEvent}
onClick={handleSelectAll}
kbd="#A"
id="TD-MenuItem-Select_All"
>
Select All
</DMItem>
<DMItem onSelect={preventEvent} onClick={handleSelectNone}>
<DMItem
onSelect={preventEvent}
onClick={handleSelectNone}
id="TD-MenuItem-Select_None"
>
Select None
</DMItem>
</DMSubMenu>
</>
)}
<a href="https://tldraw.com/r">
<DMItem>Create a Multiplayer Room</DMItem>
<DMItem id="TD-MenuItem-Create_a_Multiplayer_Room">Create a Multiplayer Room</DMItem>
</a>
<DMDivider dir="ltr" />
<PreferencesMenu />
<DMDivider dir="ltr" />
<a href="https://github.com/Tldraw/Tldraw" target="_blank" rel="nofollow">
<DMItem>
<DMItem id="TD-MenuItem-Github">
GitHub
<SmallIcon>
<GitHubLogoIcon />
@ -230,7 +277,7 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
</DMItem>
</a>
<a href="https://twitter.com/Tldraw" target="_blank" rel="nofollow">
<DMItem>
<DMItem id="TD-MenuItem-Twitter">
Twitter
<SmallIcon>
<TwitterLogoIcon />
@ -238,7 +285,7 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
</DMItem>
</a>
<a href="https://discord.gg/SBBEVCA4PG" target="_blank" rel="nofollow">
<DMItem>
<DMItem id="TD-MenuItem-Discord">
Discord
<SmallIcon>
<DiscordIcon />
@ -247,7 +294,7 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
</a>
{showSponsorLink && (
<a href="https://github.com/sponsors/steveruizok" target="_blank" rel="nofollow">
<DMItem isSponsor>
<DMItem isSponsor id="TD-MenuItem-Become_a_Sponsor">
Become a Sponsor{' '}
<SmallIcon>
<HeartIcon />
@ -258,9 +305,13 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
{showSignInOutMenu && (
<>
<DMDivider dir="ltr" />{' '}
{app.callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
{app.callbacks.onSignIn && (
<DMItem onSelect={handleSignIn} id="TD-MenuItem-Sign_in">
Sign In
</DMItem>
)}
{app.callbacks.onSignOut && (
<DMItem onSelect={handleSignOut}>
<DMItem onSelect={handleSignOut} id="TD-MenuItem-Sign_out">
Sign Out
<SmallIcon>
<ExitIcon />

View file

@ -47,7 +47,7 @@ export function PageMenu(): JSX.Element {
return (
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
<DropdownMenu.Trigger dir="ltr" asChild>
<DropdownMenu.Trigger dir="ltr" asChild id="TD-Page">
<ToolButton variant="text">{currentPageName || 'Page'}</ToolButton>
</DropdownMenu.Trigger>
<DMContent variant="menu" align="start">

View file

@ -43,37 +43,65 @@ export function PreferencesMenu() {
}, [app])
return (
<DMSubMenu label="Preferences">
<DMCheckboxItem checked={settings.isDarkMode} onCheckedChange={toggleDarkMode} kbd="#⇧D">
<DMSubMenu label="Preferences" id="TD-MenuItem-Preferences">
<DMCheckboxItem
checked={settings.isDarkMode}
onCheckedChange={toggleDarkMode}
kbd="#⇧D"
id="TD-MenuItem-Preferences-Dark_Mode"
>
Dark Mode
</DMCheckboxItem>
<DMCheckboxItem checked={settings.isFocusMode} onCheckedChange={toggleFocusMode} kbd="#.">
<DMCheckboxItem
checked={settings.isFocusMode}
onCheckedChange={toggleFocusMode}
kbd="#."
id="TD-MenuItem-Preferences-Focus_Mode"
>
Focus Mode
</DMCheckboxItem>
<DMCheckboxItem checked={settings.isDebugMode} onCheckedChange={toggleDebugMode}>
<DMCheckboxItem
checked={settings.isDebugMode}
onCheckedChange={toggleDebugMode}
id="TD-MenuItem-Preferences-Debug_Mode"
>
Debug Mode
</DMCheckboxItem>
<DMDivider />
<DMCheckboxItem checked={settings.showRotateHandles} onCheckedChange={toggleRotateHandle}>
<DMCheckboxItem
checked={settings.showRotateHandles}
onCheckedChange={toggleRotateHandle}
id="TD-MenuItem-Preferences-Rotate_Handles"
>
Rotate Handles
</DMCheckboxItem>
<DMCheckboxItem
checked={settings.showBindingHandles}
onCheckedChange={toggleBoundShapesHandle}
id="TD-MenuItem-Preferences-Binding_Handles"
>
Binding Handles
</DMCheckboxItem>
<DMCheckboxItem checked={settings.showCloneHandles} onCheckedChange={toggleCloneControls}>
<DMCheckboxItem
checked={settings.showCloneHandles}
onCheckedChange={toggleCloneControls}
id="TD-MenuItem-Preferences-Clone_Handles"
>
Clone Handles
</DMCheckboxItem>
<DMCheckboxItem
checked={settings.showGrid}
onCheckedChange={toggleGrid}
kbd="#⇧G"
id="TD-MenuItem-Preferences-Grid"
>
Grid
</DMCheckboxItem>
<DMCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
<DMCheckboxItem
checked={settings.isSnapping}
onCheckedChange={toggleisSnapping}
id="TD-MenuItem-Preferences-Always_Show_Snaps"
>
Always Show Snaps
</DMCheckboxItem>
</DMSubMenu>

View file

@ -167,7 +167,7 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
)
return (
<DropdownMenu.Root dir="ltr" onOpenChange={handleMenuOpenChange}>
<DropdownMenu.Trigger asChild>
<DropdownMenu.Trigger asChild id="TD-Styles">
<ToolButton variant="text">
Styles
<OverlapIcons
@ -187,11 +187,16 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
</ToolButton>
</DropdownMenu.Trigger>
<DMContent>
<StyledRow variant="tall">
<StyledRow variant="tall" id="TD-Styles-Color-Container">
<span>Color</span>
<ColorGrid>
{Object.keys(strokes.light).map((style: string) => (
<DropdownMenu.Item key={style} onSelect={preventEvent} asChild>
<DropdownMenu.Item
key={style}
onSelect={preventEvent}
asChild
id={`TD-Styles-Color-Swatch-${style}`}
>
<ToolButton
variant="icon"
isActive={displayedStyle.color === style}
@ -214,10 +219,11 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
variant="styleMenu"
checked={!!displayedStyle.isFilled}
onCheckedChange={handleToggleFilled}
id="TD-Styles-Fill"
>
Fill
</DMCheckboxItem>
<StyledRow>
<StyledRow id="TD-Styles-Dash-Container">
Dash
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
{Object.values(DashStyle).map((style) => (
@ -227,13 +233,14 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
value={style}
onSelect={preventEvent}
bp={breakpoints}
id={`TD-Styles-Dash-${style}`}
>
{DASH_ICONS[style as DashStyle]}
</DMRadioItem>
))}
</StyledGroup>
</StyledRow>
<StyledRow>
<StyledRow id="TD-Styles-Size-Container">
Size
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
{Object.values(SizeStyle).map((sizeStyle) => (
@ -243,6 +250,7 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
value={sizeStyle}
onSelect={preventEvent}
bp={breakpoints}
id={`TD-Styles-Dash-${sizeStyle}`}
>
{SIZE_ICONS[sizeStyle as SizeStyle]}
</DMRadioItem>
@ -252,7 +260,7 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
{(options === 'text' || options === 'label') && (
<>
<Divider />
<StyledRow>
<StyledRow id="TD-Styles-Font-Container">
Font
<StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}>
{Object.values(FontStyle).map((fontStyle) => (
@ -262,6 +270,7 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
value={fontStyle}
onSelect={preventEvent}
bp={breakpoints}
id={`TD-Styles-Font-${fontStyle}`}
>
<FontIcon fontStyle={fontStyle}>Aa</FontIcon>
</DMRadioItem>
@ -269,7 +278,7 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
</StyledGroup>
</StyledRow>
{options === 'text' && (
<StyledRow>
<StyledRow id="TD-Styles-Align-Container">
Align
<StyledGroup
dir="ltr"
@ -283,6 +292,7 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
value={style}
onSelect={preventEvent}
bp={breakpoints}
id={`TD-Styles-Align-${style}`}
>
{ALIGN_ICONS[style]}
</DMRadioItem>

View file

@ -32,7 +32,7 @@ export function TopPanel({
return (
<StyledTopPanel>
{(showMenu || showPages) && (
<Panel side="left">
<Panel side="left" id="TD-MenuPanel">
{showMenu && <Menu showSponsorLink={showSponsorLink} readOnly={readOnly} />}
{showPages && <PageMenu />}
</Panel>

View file

@ -16,25 +16,30 @@ export const ZoomMenu = React.memo(function ZoomMenu() {
return (
<DropdownMenu.Root dir="ltr">
<DropdownMenu.Trigger dir="ltr" asChild>
<DropdownMenu.Trigger dir="ltr" asChild id="TD-Zoom">
<FixedWidthToolButton onDoubleClick={app.resetZoom} variant="text">
{Math.round(zoom * 100)}%
</FixedWidthToolButton>
</DropdownMenu.Trigger>
<DMContent align="end">
<DMItem onSelect={preventEvent} onClick={app.zoomIn} kbd="#+">
<DMItem onSelect={preventEvent} onClick={app.zoomIn} kbd="#+" id="TD-Zoom-Zoom_In">
Zoom In
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.zoomOut} kbd="#">
<DMItem onSelect={preventEvent} onClick={app.zoomOut} kbd="#" id="TD-Zoom-Zoom_Out">
Zoom Out
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.resetZoom} kbd="⇧0">
<DMItem onSelect={preventEvent} onClick={app.resetZoom} kbd="⇧0" id="TD-Zoom-Zoom_To_100%">
To 100%
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.zoomToFit} kbd="⇧1">
<DMItem onSelect={preventEvent} onClick={app.zoomToFit} kbd="⇧1" id="TD-Zoom-To_Fit">
To Fit
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.zoomToSelection} kbd="⇧2">
<DMItem
onSelect={preventEvent}
onClick={app.zoomToSelection}
kbd="⇧2"
id="TD-Zoom-To_Selection"
>
To Selection
</DMItem>
</DMContent>