Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'
|
|
import {
|
|
TldrawUiButton,
|
|
TldrawUiDropdownMenuContent,
|
|
TldrawUiDropdownMenuRoot,
|
|
TldrawUiDropdownMenuTrigger,
|
|
tlmenus,
|
|
useMaybeEditor,
|
|
useTranslation,
|
|
} from 'tldraw'
|
|
import { EmojiPicker, type EmojiPickerProps } from './emoji-picker'
|
|
import { SmileyIcon } from './icons'
|
|
import { RenderReaction } from './reaction'
|
|
|
|
/** @public */
|
|
export interface ReactionPickerProps {
|
|
/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */
|
|
emoji?: string[]
|
|
/** Emoji the current user has already reacted with; shown as pressed in the grid. */
|
|
selected?: string[]
|
|
/** Called when an emoji is chosen. */
|
|
onSelect?(emoji: string): void
|
|
/** How to draw each emoji token. Defaults to the token string (OS emoji font). */
|
|
renderReaction?: RenderReaction
|
|
/**
|
|
* What the button opens — the thing that produces a token. Defaults to `EmojiPicker`, the grid of
|
|
* emoji. Swap it for any component taking the same props to offer something else entirely (the
|
|
* drawn reactions example replaces it with a canvas you draw in); the props are passed straight
|
|
* through either way.
|
|
*/
|
|
palette?: ComponentType<EmojiPickerProps>
|
|
/**
|
|
* Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.
|
|
* A thread renders one picker per comment, so this must differ per comment — sharing an id
|
|
* makes every picker in the thread open at once. Defaults to a per-instance generated id;
|
|
* pass one only for a stabler, more debuggable value.
|
|
*/
|
|
menuId?: string
|
|
/** Class for the trigger button. Defaults to the card-action style, matching the ⋯ button. */
|
|
className?: string
|
|
}
|
|
|
|
/**
|
|
* Whether a palette pick asked to keep the palette open (shift held) for multi-select.
|
|
* @internal
|
|
*/
|
|
export function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {
|
|
return !!event?.shiftKey
|
|
}
|
|
|
|
/**
|
|
* The add-reaction affordance: a smiley button that opens an emoji grid.
|
|
*
|
|
* Anchored to its own button rather than to the reactions row, so the menu keeps its position as
|
|
* reactions are added and the row reflows.
|
|
*
|
|
* Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
|
|
* done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
|
|
* choosing several reactions in a row.
|
|
* @public @react
|
|
*/
|
|
export function ReactionPicker({
|
|
emoji,
|
|
selected,
|
|
onSelect,
|
|
renderReaction,
|
|
palette: Palette = EmojiPicker,
|
|
menuId,
|
|
className = 'tlui-cmt-thread__action',
|
|
}: ReactionPickerProps) {
|
|
const msg = useTranslation()
|
|
const editor = useMaybeEditor()
|
|
// Unique per mounted picker unless the host supplies something stabler — see `menuId`.
|
|
const generatedId = useId()
|
|
const id = menuId ?? `comment-reactions-${generatedId}`
|
|
|
|
// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is
|
|
// what closes it. The palette's tokens are plain buttons — a swappable component, not radix
|
|
// menu items — so nothing dismisses the menu on its own.
|
|
const handleSelect = useCallback(
|
|
(value: string, event?: MouseEvent | KeyboardEvent) => {
|
|
onSelect?.(value)
|
|
if (!isMultiSelectPick(event)) {
|
|
tlmenus.deleteOpenMenu(id, editor?.contextId)
|
|
}
|
|
},
|
|
[onSelect, id, editor]
|
|
)
|
|
|
|
return (
|
|
<TldrawUiDropdownMenuRoot id={id}>
|
|
<TldrawUiDropdownMenuTrigger>
|
|
<TldrawUiButton
|
|
type="icon"
|
|
tooltip={msg('comments.add-reaction')}
|
|
title={msg('comments.add-reaction')}
|
|
className={className}
|
|
>
|
|
<SmileyIcon />
|
|
</TldrawUiButton>
|
|
</TldrawUiDropdownMenuTrigger>
|
|
{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}
|
|
<TldrawUiDropdownMenuContent side="bottom" align="start" alignOffset={0} sideOffset={4}>
|
|
<Palette
|
|
emoji={emoji}
|
|
selected={selected}
|
|
onSelect={handleSelect}
|
|
renderReaction={renderReaction}
|
|
/>
|
|
</TldrawUiDropdownMenuContent>
|
|
</TldrawUiDropdownMenuRoot>
|
|
)
|
|
}
|