// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ForwardedRef, ReactNode, DOMAttributes, PointerEvent, } from 'react'; import React, { forwardRef, useCallback, useEffect, useMemo } from 'react'; import { mergeProps } from '@react-aria/utils'; import { strictAssert } from '../../../util/assert'; /** * Button */ export type FunItemButtonLongPressProps = Readonly< | { longPressAccessibilityDescription?: never; onLongPress?: never; } | { longPressAccessibilityDescription: string; onLongPress: (event: LongPressEvent) => void; } >; export type FunItemButtonProps = Readonly< { 'aria-label': string; excludeFromTabOrder: boolean; onClick: (event: PointerEvent) => void; onContextMenu?: (event: PointerEvent) => void; children: ReactNode; } & FunItemButtonLongPressProps >; export const FunItemButton = forwardRef(function FunItemButton( props: FunItemButtonProps, ref: ForwardedRef ): JSX.Element { const { 'aria-label': ariaLabel, excludeFromTabOrder, onClick, onContextMenu, children, longPressAccessibilityDescription, onLongPress, ...rest } = props; const longPressProps = useLongPress(onLongPress ?? null); const handleClick = useCallback( (event: PointerEvent) => { if (!event.defaultPrevented) { onClick(event); } }, [onClick] ); return ( // eslint-disable-next-line jsx-a11y/role-supports-aria-props ); }); type LongPressEvent = Readonly<{ pointerType: PointerEvent['pointerType']; }>; function useLongPress( onLongPress: ((event: LongPressEvent) => void) | null ): DOMAttributes { const { cleanup, props } = useMemo(() => { if (onLongPress == null) { return { props: {} }; } let timer: ReturnType; let isLongPressed = false; let lastLongPress: number | null = null; function reset() { clearTimeout(timer); isLongPressed = false; } function handleCancel(event: PointerEvent) { if (isLongPressed) { lastLongPress = event.timeStamp; } reset(); } function handleStart(event: PointerEvent) { const press: LongPressEvent = { pointerType: event.pointerType }; reset(); timer = setTimeout(() => { isLongPressed = true; strictAssert(onLongPress != null, 'Missing callback'); onLongPress(press); }, 500); } function handleClick(event: PointerEvent) { if (event.timeStamp === lastLongPress) { event.preventDefault(); } } return { cleanup: reset, props: { onPointerDown: handleStart, onPointerUp: handleCancel, onPointerCancel: handleCancel, onPointerLeave: handleCancel, onClick: handleClick, } satisfies DOMAttributes, }; }, [onLongPress]); useEffect(() => { return cleanup; }, [cleanup]); return props; }