tldraw/packages/tldraw/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx
Lu Wilson 8bc108462a
Improve dialog appearance on small components (#2884)
This PR fixes and improves the appearance on dialogs on small tldraw
components, eg: Inline components.

Fixes TLD-2232


![image](https://github.com/tldraw/tldraw/assets/15892272/0fae3be9-4a52-45f3-a107-529e101aa4bd)


![image](https://github.com/tldraw/tldraw/assets/15892272/eb0ad67f-b390-4738-885a-65c968d7c989)

![image](https://github.com/tldraw/tldraw/assets/15892272/24946c06-4762-4e51-8113-797be2203f79)


![image](https://github.com/tldraw/tldraw/assets/15892272/0d646044-c8a5-4b05-9530-5f3758767d0d)

Marking as minor instead of patch because it adds a new prop to
`TldrawUiKbd`.

### Change Type

- [ ] `patch` — Bug fix
- [x] `minor` — New feature
- [ ] `major` — Breaking change
- [ ] `dependencies` — Changes to package dependencies[^1]
- [ ] `documentation` — Changes to the documentation only[^2]
- [ ] `tests` — Changes to any test code only[^2]
- [ ] `internal` — Any other changes that don't affect the published
package[^2]
- [ ] I don't know

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Test Plan

1. Open the "Inset editor" example.
2. Open the keyboard shortcuts dialog.
3. Shrink the window down.
4. Make sure the dialog remains visible at all window sizes.

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Dev: Made default dialogs work better when used in small components.
2024-02-22 12:42:01 +00:00

203 lines
5.4 KiB
TypeScript

import { ContextMenuItem } from '@radix-ui/react-context-menu'
import { preventDefault } from '@tldraw/editor'
import { useState } from 'react'
import { unwrapLabel } from '../../../context/actions'
import { TLUiEventSource } from '../../../context/events'
import { useReadonly } from '../../../hooks/useReadonly'
import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
import { kbdStr } from '../../../kbd-utils'
import { Spinner } from '../../Spinner'
import { TldrawUiButton } from '../Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../Button/TldrawUiButtonIcon'
import { TldrawUiButtonLabel } from '../Button/TldrawUiButtonLabel'
import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
import { TldrawUiKbd } from '../TldrawUiKbd'
import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
/** @public */
export type TLUiMenuItemProps<
TranslationKey extends string = string,
IconType extends string = string,
> = {
id: string
/**
* The icon to display on the item.
*/
icon?: IconType
/**
* The keyboard shortcut to display on the item.
*/
kbd?: string
/**
* The title to display on the item.
*/
title?: string
/**
* The label to display on the item. If it's a string, it will be translated. If it's an object, the keys will be used as the language keys and the values will be translated.
*/
label?: TranslationKey | { [key: string]: TranslationKey }
/**
* If the editor is in readonly mode and the item is not marked as readonlyok, it will not be rendered.
*/
readonlyOk?: boolean
/**
* The function to call when the item is clicked.
*/
onSelect: (source: TLUiEventSource) => Promise<void> | void
/**
* Whether this item should be disabled.
*/
disabled?: boolean
/**
* Prevent the menu from closing when the item is clicked
*/
noClose?: boolean
/**
* Whether to show a spinner on the item.
*/
spinner?: boolean
}
/** @public */
export function TldrawUiMenuItem<
TranslationKey extends string = string,
IconType extends string = string,
>({
disabled = false,
spinner = false,
readonlyOk = false,
id,
kbd,
label,
icon,
onSelect,
noClose,
}: TLUiMenuItemProps<TranslationKey, IconType>) {
const { type: menuType, sourceId } = useTldrawUiMenuContext()
const msg = useTranslation()
const [disableClicks, setDisableClicks] = useState(false)
const isReadonlyMode = useReadonly()
if (isReadonlyMode && !readonlyOk) return null
const labelToUse = unwrapLabel(label, menuType)
const kbdTouse = kbd ? kbdStr(kbd) : undefined
const labelStr = labelToUse ? msg(labelToUse as TLUiTranslationKey) : undefined
const titleStr = labelStr && kbdTouse ? `${labelStr} ${kbdTouse}` : labelStr
switch (menuType) {
case 'menu': {
return (
<TldrawUiDropdownMenuItem>
<TldrawUiButton
type="menu"
data-testid={`${sourceId}.${id}`}
disabled={disabled}
title={titleStr}
onClick={(e) => {
if (noClose) {
preventDefault(e)
}
if (disableClicks) {
setDisableClicks(false)
} else {
onSelect(sourceId)
}
}}
>
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
{kbd && <TldrawUiKbd>{kbd}</TldrawUiKbd>}
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
)
}
case 'context-menu': {
// Hide disabled context menu items
if (disabled) return null
return (
<ContextMenuItem
dir="ltr"
title={titleStr}
draggable={false}
className="tlui-button tlui-button__menu"
data-testid={`${sourceId}.${id}`}
onSelect={(e) => {
if (noClose) preventDefault(e)
if (disableClicks) {
setDisableClicks(false)
} else {
onSelect(sourceId)
}
}}
>
<span className="tlui-button__label" draggable={false}>
{labelStr}
</span>
{kbd && <TldrawUiKbd>{kbd}</TldrawUiKbd>}
{spinner && <Spinner />}
</ContextMenuItem>
)
}
case 'panel': {
return (
<TldrawUiButton
data-testid={`${sourceId}.${id}`}
type="menu"
title={titleStr}
disabled={disabled}
onClick={() => onSelect(sourceId)}
>
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
{icon && <TldrawUiButtonIcon icon={icon} />}
</TldrawUiButton>
)
}
case 'small-icons':
case 'icons': {
return (
<TldrawUiButton
data-testid={`${sourceId}.${id}`}
type="icon"
title={titleStr}
disabled={disabled}
onClick={() => onSelect(sourceId)}
>
<TldrawUiButtonIcon icon={icon!} small={menuType === 'small-icons'} />
</TldrawUiButton>
)
}
case 'keyboard-shortcuts': {
if (!kbd) {
console.warn(
`Menu item '${label}' isn't shown in the keyboard shortcuts dialog because it doesn't have a keyboard shortcut.`
)
return null
}
return (
<div className="tlui-shortcuts-dialog__key-pair" data-testid={`${sourceId}.${id}`}>
<div className="tlui-shortcuts-dialog__key-pair__key">{labelStr}</div>
<div className="tlui-shortcuts-dialog__key-pair__value">
<TldrawUiKbd visibleOnMobileLayout>{kbd}</TldrawUiKbd>
</div>
</div>
)
}
case 'helper-buttons': {
return (
<TldrawUiButton type="low" onClick={() => onSelect(sourceId)}>
<TldrawUiButtonIcon icon={icon!} />
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
</TldrawUiButton>
)
}
default: {
return null
}
}
}