Enable hotkeys for toolbar items within dropdowns (#6765)
* Remove gnarly fake union hotkeys * Enable hotkey for items buried in ActionButtonDropdown I'm kinda over `useHotkeys` as a hook * Add hotkeys for other sketch tools * Fix lint and tsc * Fix duplicate locator * The circular dependecies got reordered somehow * Update src/lib/toolbar.ts
This commit is contained in:
@ -1445,7 +1445,7 @@ part001 = startSketchOn(XZ)
|
|||||||
|
|
||||||
await page.getByTestId('overlay-menu').click()
|
await page.getByTestId('overlay-menu').click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.getByText('Remove constraints').click()
|
await page.getByRole('button', { name: 'Remove constraints' }).click()
|
||||||
|
|
||||||
await editor.expectEditor.toContain(after, { shouldNormalise: true })
|
await editor.expectEditor.toContain(after, { shouldNormalise: true })
|
||||||
|
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx
|
> dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx
|
||||||
|
|
||||||
• Circular Dependencies
|
• Circular Dependencies
|
||||||
1) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
|
1) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
||||||
2) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
|
2) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
||||||
3) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/setAngleLength.tsx -> src/components/SetAngleLengthModal.tsx -> src/lib/useCalculateKclExpression.ts
|
3) src/lib/singletons.ts -> src/lang/codeManager.ts
|
||||||
4) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
4) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
|
||||||
5) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
5) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
|
||||||
6) src/lib/singletons.ts -> src/lang/codeManager.ts
|
6) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
|
||||||
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
|
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts
|
||||||
8) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts
|
8) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/setAngleLength.tsx -> src/components/SetAngleLengthModal.tsx -> src/lib/useCalculateKclExpression.ts
|
||||||
|
@ -25,8 +25,8 @@ import type {
|
|||||||
ToolbarModeName,
|
ToolbarModeName,
|
||||||
} from '@src/lib/toolbar'
|
} from '@src/lib/toolbar'
|
||||||
import { isToolbarItemResolvedDropdown, toolbarConfig } from '@src/lib/toolbar'
|
import { isToolbarItemResolvedDropdown, toolbarConfig } from '@src/lib/toolbar'
|
||||||
import { isArray } from '@src/lib/utils'
|
|
||||||
import { commandBarActor } from '@src/lib/singletons'
|
import { commandBarActor } from '@src/lib/singletons'
|
||||||
|
import { filterEscHotkey } from '@src/lib/hotkeyWrapper'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -253,7 +253,8 @@ export function Toolbar({
|
|||||||
!['available', 'experimental'].includes(
|
!['available', 'experimental'].includes(
|
||||||
itemConfig.status
|
itemConfig.status
|
||||||
) ||
|
) ||
|
||||||
itemConfig.disabled === true,
|
itemConfig.disabled === true ||
|
||||||
|
itemConfig.disableHotkey === true,
|
||||||
status: itemConfig.status,
|
status: itemConfig.status,
|
||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
@ -410,6 +411,10 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
|||||||
contentClassName = '',
|
contentClassName = '',
|
||||||
children,
|
children,
|
||||||
}: ToolbarItemContentsProps) {
|
}: ToolbarItemContentsProps) {
|
||||||
|
/**
|
||||||
|
* GOTCHA: `useHotkeys` can only register one hotkey listener per component.
|
||||||
|
* TODO: make a global hotkey registration system. make them editable.
|
||||||
|
*/
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
itemConfig.hotkey || '',
|
itemConfig.hotkey || '',
|
||||||
() => {
|
() => {
|
||||||
@ -469,7 +474,7 @@ const ToolbarItemTooltipShortContent = ({
|
|||||||
{title}
|
{title}
|
||||||
{hotkey && (
|
{hotkey && (
|
||||||
<kbd className="inline-block ml-2 flex-none hotkey">
|
<kbd className="inline-block ml-2 flex-none hotkey">
|
||||||
{displayHotkeys(hotkey)}
|
{filterEscHotkey(hotkey)}
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -510,7 +515,7 @@ const ToolbarItemTooltipRichContent = ({
|
|||||||
</div>
|
</div>
|
||||||
{shouldBeEnabled && itemConfig.hotkey ? (
|
{shouldBeEnabled && itemConfig.hotkey ? (
|
||||||
<kbd className="flex-none hotkey">
|
<kbd className="flex-none hotkey">
|
||||||
{displayHotkeys(itemConfig.hotkey)}
|
{filterEscHotkey(itemConfig.hotkey)}
|
||||||
</kbd>
|
</kbd>
|
||||||
) : itemConfig.status === 'kcl-only' ? (
|
) : itemConfig.status === 'kcl-only' ? (
|
||||||
<>
|
<>
|
||||||
@ -573,11 +578,6 @@ const ToolbarItemTooltipRichContent = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to display Esc hotkeys to avoid confusion in the Toolbar UI (eg. "EscR")
|
|
||||||
function displayHotkeys(hotkey: string | string[]) {
|
|
||||||
return (isArray(hotkey) ? hotkey : [hotkey]).filter((h) => h !== 'Esc')
|
|
||||||
}
|
|
||||||
|
|
||||||
function isToolbarDropdown(
|
function isToolbarDropdown(
|
||||||
item: ToolbarItem | ToolbarDropdown
|
item: ToolbarItem | ToolbarDropdown
|
||||||
): item is ToolbarDropdown {
|
): item is ToolbarDropdown {
|
||||||
|
@ -3,6 +3,8 @@ import { Popover } from '@headlessui/react'
|
|||||||
import type { ActionButtonProps } from '@src/components/ActionButton'
|
import type { ActionButtonProps } from '@src/components/ActionButton'
|
||||||
import { CustomIcon } from '@src/components/CustomIcon'
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
import Tooltip from '@src/components/Tooltip'
|
import Tooltip from '@src/components/Tooltip'
|
||||||
|
import { filterEscHotkey } from '@src/lib/hotkeyWrapper'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||||
name?: string
|
name?: string
|
||||||
@ -10,7 +12,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
|||||||
splitMenuItems: {
|
splitMenuItems: {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
shortcut?: string
|
hotkey?: string | string[]
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
status?: 'available' | 'unavailable' | 'kcl-only' | 'experimental'
|
status?: 'available' | 'unavailable' | 'kcl-only' | 'experimental'
|
||||||
@ -63,24 +65,58 @@ export function ActionButtonDropdown({
|
|||||||
<Popover.Panel
|
<Popover.Panel
|
||||||
as="ul"
|
as="ul"
|
||||||
className="!pointer-events-auto absolute z-20 left-1/2 -translate-x-1/2 top-full mt-4 w-fit max-w-[280px] max-h-[80vh] overflow-y-auto py-2 flex flex-col align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
|
className="!pointer-events-auto absolute z-20 left-1/2 -translate-x-1/2 top-full mt-4 w-fit max-w-[280px] max-h-[80vh] overflow-y-auto py-2 flex flex-col align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
|
||||||
|
unmount={false}
|
||||||
>
|
>
|
||||||
{splitMenuItems.map((item) => (
|
{splitMenuItems.map((item, index) => (
|
||||||
<li className="contents" key={item.label}>
|
<ActionButtonDropdownListItem
|
||||||
<button
|
item={item}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
item.onClick()
|
item.onClick()
|
||||||
// Close the popover
|
// Close the popover
|
||||||
close()
|
close()
|
||||||
}}
|
}}
|
||||||
|
key={item.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Popover.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionButtonDropdownListItem({
|
||||||
|
item,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
item: ActionButtonSplitProps['splitMenuItems'][number]
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
/**
|
||||||
|
* GOTCHA: `useHotkeys` can only register one hotkey listener per component.
|
||||||
|
* and since the first item in the dropdown has a top-level button too,
|
||||||
|
* it already has a hotkey listener, so we should skip it.
|
||||||
|
* TODO: make a global hotkey registration system. make them editable.
|
||||||
|
*/
|
||||||
|
useHotkeys(item.hotkey || '', item.onClick, {
|
||||||
|
enabled:
|
||||||
|
['available', 'experimental'].includes(item.status || '') &&
|
||||||
|
!!item.hotkey &&
|
||||||
|
!item.disabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="contents">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
data-testid={'dropdown-' + item.id}
|
data-testid={'dropdown-' + item.id}
|
||||||
data-onboarding-id={`${props.name}-dropdown-item`}
|
data-onboarding-id={`${item.id}-dropdown-item`}
|
||||||
>
|
>
|
||||||
<span className="capitalize flex-grow text-left">
|
<span className="capitalize flex-grow text-left">{item.label}</span>
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
{item.status === 'unavailable' ? (
|
{item.status === 'unavailable' ? (
|
||||||
<div className="flex flex-none items-center gap-1">
|
<div className="flex flex-none items-center gap-1">
|
||||||
<span className="text-chalkboard-70 dark:text-chalkboard-40">
|
<span className="text-chalkboard-70 dark:text-chalkboard-40">
|
||||||
@ -101,9 +137,9 @@ export function ActionButtonDropdown({
|
|||||||
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
|
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : item.shortcut ? (
|
) : item.hotkey ? (
|
||||||
<kbd className="hotkey flex-none group-disabled/button:text-chalkboard-50 dark:group-disabled/button:text-chalkboard-70 group-disabled/button:border-chalkboard-20 dark:group-disabled/button:border-chalkboard-80">
|
<kbd className="hotkey flex-none group-disabled/button:text-chalkboard-50 dark:group-disabled/button:text-chalkboard-70 group-disabled/button:border-chalkboard-20 dark:group-disabled/button:border-chalkboard-80">
|
||||||
{item.shortcut}
|
{filterEscHotkey(item.hotkey)}
|
||||||
</kbd>
|
</kbd>
|
||||||
) : null}
|
) : null}
|
||||||
{item.status === 'experimental' ? (
|
{item.status === 'experimental' ? (
|
||||||
@ -111,10 +147,5 @@ export function ActionButtonDropdown({
|
|||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
|
||||||
</Popover.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import type { Options } from 'react-hotkeys-hook'
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
import { codeManager } from '@src/lib/singletons'
|
import { codeManager } from '@src/lib/singletons'
|
||||||
import type { Platform } from '@src/lib/utils'
|
import { isArray, type Platform } from '@src/lib/utils'
|
||||||
|
|
||||||
// Hotkey wrapper wraps hotkeys for the app (outside of the editor)
|
// Hotkey wrapper wraps hotkeys for the app (outside of the editor)
|
||||||
// with hotkeys inside the editor.
|
// with hotkeys inside the editor.
|
||||||
@ -86,3 +86,10 @@ export function hotkeyDisplay(hotkey: string, platform: Platform): string {
|
|||||||
|
|
||||||
return display
|
return display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We don't want to display Esc hotkeys to avoid confusion in the Toolbar UI (eg. "EscR")
|
||||||
|
*/
|
||||||
|
export function filterEscHotkey(hotkey: string | string[]) {
|
||||||
|
return (isArray(hotkey) ? hotkey : [hotkey]).filter((h) => h !== 'Esc')
|
||||||
|
}
|
||||||
|
@ -238,7 +238,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
icon: 'booleanUnion',
|
icon: 'booleanUnion',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Union',
|
title: 'Union',
|
||||||
hotkey: 'Shift + B U',
|
|
||||||
description: 'Combine two or more solids into a single solid.',
|
description: 'Combine two or more solids into a single solid.',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -257,7 +256,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
icon: 'booleanSubtract',
|
icon: 'booleanSubtract',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Subtract',
|
title: 'Subtract',
|
||||||
hotkey: 'Shift + B S',
|
|
||||||
description: 'Subtract one solid from another.',
|
description: 'Subtract one solid from another.',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -276,7 +274,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
icon: 'booleanIntersect',
|
icon: 'booleanIntersect',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Intersect',
|
title: 'Intersect',
|
||||||
hotkey: 'Shift + B I',
|
|
||||||
description: 'Create a solid from the intersection of two solids.',
|
description: 'Create a solid from the intersection of two solids.',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -631,7 +628,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
isActive: (state) =>
|
isActive: (state) =>
|
||||||
state.matches({ Sketch: 'Circle three point tool' }),
|
state.matches({ Sketch: 'Circle three point tool' }),
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches({ Sketch: 'Circle three point tool' }) ? 'Esc' : [],
|
state.matches({ Sketch: 'Circle three point tool' })
|
||||||
|
? ['Alt+C', 'Esc']
|
||||||
|
: 'Alt+C',
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
description: 'Draw a circle defined by three points',
|
description: 'Draw a circle defined by three points',
|
||||||
links: [],
|
links: [],
|
||||||
@ -681,6 +680,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
title: 'Center rectangle',
|
title: 'Center rectangle',
|
||||||
description: 'Start drawing a rectangle from its center',
|
description: 'Start drawing a rectangle from its center',
|
||||||
links: [],
|
links: [],
|
||||||
|
hotkey: (state) =>
|
||||||
|
state.matches({ Sketch: 'Center Rectangle tool' })
|
||||||
|
? ['Alt+R', 'Esc']
|
||||||
|
: 'Alt+R',
|
||||||
isActive: (state) => {
|
isActive: (state) => {
|
||||||
return state.matches({ Sketch: 'Center Rectangle tool' })
|
return state.matches({ Sketch: 'Center Rectangle tool' })
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user