mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-25 04:36:46 +00:00
Fix FunLightbox and FunTooltip
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ForwardedRef, ReactNode } from 'react';
|
||||
import React, { forwardRef, useEffect, useRef } from 'react';
|
||||
import { type PressEvent, useLongPress } from 'react-aria';
|
||||
import type { LongPressEvent } from '@react-types/shared';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { mergeRefs } from '@react-aria/utils';
|
||||
import { PressResponder } from '@react-aria/interactions';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -28,49 +29,120 @@ export type FunItemButtonProps = Readonly<
|
||||
{
|
||||
'aria-label': string;
|
||||
excludeFromTabOrder: boolean;
|
||||
onPress: (event: PressEvent) => void;
|
||||
onContextMenu?: (event: MouseEvent) => void;
|
||||
onClick: (event: PointerEvent) => void;
|
||||
onContextMenu?: (event: PointerEvent) => void;
|
||||
children: ReactNode;
|
||||
} & FunItemButtonLongPressProps
|
||||
>;
|
||||
|
||||
export const FunItemButton = forwardRef(function FunItemButton(
|
||||
props: FunItemButtonProps,
|
||||
outerRef: ForwardedRef<HTMLButtonElement>
|
||||
ref: ForwardedRef<HTMLButtonElement>
|
||||
): JSX.Element {
|
||||
const { onContextMenu } = props;
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
const {
|
||||
'aria-label': ariaLabel,
|
||||
excludeFromTabOrder,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
children,
|
||||
longPressAccessibilityDescription,
|
||||
onLongPress,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const { longPressProps } = useLongPress({
|
||||
isDisabled: props.onLongPress == null,
|
||||
accessibilityDescription: props.longPressAccessibilityDescription,
|
||||
onLongPress: props.onLongPress,
|
||||
});
|
||||
const longPressProps = useLongPress(onLongPress ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
strictAssert(innerRef.current, 'Missing ref element');
|
||||
const element = innerRef.current;
|
||||
if (onContextMenu == null) {
|
||||
return () => null;
|
||||
}
|
||||
element.addEventListener('contextmenu', onContextMenu);
|
||||
return () => {
|
||||
element.removeEventListener('contextmenu', onContextMenu);
|
||||
};
|
||||
}, [onContextMenu]);
|
||||
const handleClick = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (!event.defaultPrevented) {
|
||||
onClick(event);
|
||||
}
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<PressResponder {...longPressProps}>
|
||||
<Button
|
||||
ref={mergeRefs(innerRef, outerRef)}
|
||||
type="button"
|
||||
className="FunItem__Button"
|
||||
aria-label={props['aria-label']}
|
||||
excludeFromTabOrder={props.excludeFromTabOrder}
|
||||
onPress={props.onPress}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
</PressResponder>
|
||||
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className="FunItem__Button"
|
||||
aria-label={ariaLabel}
|
||||
aria-description={longPressAccessibilityDescription}
|
||||
tabIndex={excludeFromTabOrder ? -1 : undefined}
|
||||
{...mergeProps(
|
||||
longPressProps,
|
||||
{
|
||||
onClick: handleClick,
|
||||
onContextMenu,
|
||||
},
|
||||
rest
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
type LongPressEvent = Readonly<{
|
||||
pointerType: PointerEvent['pointerType'];
|
||||
}>;
|
||||
|
||||
function useLongPress(
|
||||
onLongPress: ((event: LongPressEvent) => void) | null
|
||||
): DOMAttributes<Element> {
|
||||
const { cleanup, props } = useMemo(() => {
|
||||
if (onLongPress == null) {
|
||||
return { props: {} };
|
||||
}
|
||||
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
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<Element>,
|
||||
};
|
||||
}, [onLongPress]);
|
||||
|
||||
useEffect(() => {
|
||||
return cleanup;
|
||||
}, [cleanup]);
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { useCallback } from 'react';
|
||||
import type { Placement } from 'react-aria';
|
||||
import { Dialog, Popover } from 'react-aria-components';
|
||||
import classNames from 'classnames';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { ThemeType } from '../../../types/Util';
|
||||
|
||||
export type FunPopoverProps = Readonly<{
|
||||
@@ -16,8 +17,14 @@ export type FunPopoverProps = Readonly<{
|
||||
export function FunPopover(props: FunPopoverProps): JSX.Element {
|
||||
const shouldCloseOnInteractOutside = useCallback(
|
||||
(element: Element): boolean => {
|
||||
// Don't close when quill steals focus
|
||||
const match = element.closest('.module-composition-input__input');
|
||||
const match = element.closest(
|
||||
[
|
||||
// Don't close when quill steals focus
|
||||
'.module-composition-input__input',
|
||||
// Don't close when clicking tooltip
|
||||
'.FunTooltip',
|
||||
].join(', ')
|
||||
);
|
||||
if (match != null) {
|
||||
return false;
|
||||
}
|
||||
@@ -27,16 +34,18 @@ export function FunPopover(props: FunPopoverProps): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
data-fun-overlay
|
||||
className={classNames('FunPopover', {
|
||||
'light-theme': props.theme === ThemeType.light,
|
||||
'dark-theme': props.theme === ThemeType.dark,
|
||||
})}
|
||||
placement={props.placement}
|
||||
shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
|
||||
>
|
||||
<Dialog className="FunPopover__Dialog">{props.children}</Dialog>
|
||||
</Popover>
|
||||
<Tooltip.Provider>
|
||||
<Popover
|
||||
data-fun-overlay
|
||||
className={classNames('FunPopover', {
|
||||
'light-theme': props.theme === ThemeType.light,
|
||||
'dark-theme': props.theme === ThemeType.dark,
|
||||
})}
|
||||
placement={props.placement}
|
||||
shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
|
||||
>
|
||||
<Dialog className="FunPopover__Dialog">{props.children}</Dialog>
|
||||
</Popover>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import classNames from 'classnames';
|
||||
import type { Transition } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactNode, Ref } from 'react';
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
@@ -13,6 +13,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
useId,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
import type { Selection } from 'react-aria-components';
|
||||
import { ListBox, ListBoxItem } from 'react-aria-components';
|
||||
@@ -25,6 +26,7 @@ import * as log from '../../../logging/log';
|
||||
import * as Errors from '../../../types/errors';
|
||||
import { strictAssert } from '../../../util/assert';
|
||||
import { FunImage } from './FunImage';
|
||||
import { FunTooltip } from './FunTooltip';
|
||||
|
||||
/**
|
||||
* Sub Nav
|
||||
@@ -246,11 +248,30 @@ function FunSubNavListBoxItemButton(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const FunSubNavListBoxItemTooltipTarget = forwardRef(
|
||||
function FunSubNavListBoxItemTooltipTarget(props, ref: Ref<HTMLSpanElement>) {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
{...props}
|
||||
className="FunSubNav__ListBoxItem__TooltipTarget"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export function FunSubNavListBoxItem(
|
||||
props: FunSubNavListBoxItemProps
|
||||
): JSX.Element {
|
||||
const context = useContext(FunSubNavListBoxContext);
|
||||
strictAssert(context, 'Must be wrapped with <FunSubNavListBox>');
|
||||
|
||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||
|
||||
const handleTooltipOpenChange = useCallback((open: boolean) => {
|
||||
setTooltipOpen(open);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ListBoxItem
|
||||
id={props.id}
|
||||
@@ -260,22 +281,35 @@ export function FunSubNavListBoxItem(
|
||||
>
|
||||
{({ isSelected, isFocusVisible }) => {
|
||||
return (
|
||||
<FunSubNavListBoxItemButton isSelected={isSelected}>
|
||||
<span className="FunSubNav__ListBoxItem__ButtonIcon">
|
||||
{props.children}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<motion.div
|
||||
className="FunSubNav__ListBoxItem__ButtonIndicator"
|
||||
layoutId={`FunSubNav__ListBoxItem__ButtonIndicator--${context.id}`}
|
||||
layoutDependency={context.selected}
|
||||
transition={FunSubNavListBoxItemTransition}
|
||||
/>
|
||||
)}
|
||||
{!isSelected && isFocusVisible && (
|
||||
<div className="FunSubNav__ListBoxItem__ButtonIndicator" />
|
||||
)}
|
||||
</FunSubNavListBoxItemButton>
|
||||
<>
|
||||
<FunTooltip
|
||||
open={tooltipOpen || (isSelected && isFocusVisible)}
|
||||
onOpenChange={handleTooltipOpenChange}
|
||||
side="top"
|
||||
content={props.label}
|
||||
collisionBoundarySelector=".FunPanel"
|
||||
collisionPadding={6}
|
||||
disableHoverableContent
|
||||
>
|
||||
<FunSubNavListBoxItemTooltipTarget />
|
||||
</FunTooltip>
|
||||
<FunSubNavListBoxItemButton isSelected={isSelected}>
|
||||
<span className="FunSubNav__ListBoxItem__ButtonIcon">
|
||||
{props.children}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<motion.div
|
||||
className="FunSubNav__ListBoxItem__ButtonIndicator"
|
||||
layoutId={`FunSubNav__ListBoxItem__ButtonIndicator--${context.id}`}
|
||||
layoutDependency={context.selected}
|
||||
transition={FunSubNavListBoxItemTransition}
|
||||
/>
|
||||
)}
|
||||
{!isSelected && isFocusVisible && (
|
||||
<div className="FunSubNav__ListBoxItem__ButtonIndicator" />
|
||||
)}
|
||||
</FunSubNavListBoxItemButton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ListBoxItem>
|
||||
|
||||
@@ -1,19 +1,57 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { type ReactNode } from 'react';
|
||||
import type { Placement } from 'react-aria';
|
||||
import { Tooltip } from 'react-aria-components';
|
||||
import React, { useRef, useState, type ReactNode } from 'react';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { useLayoutEffect } from '@react-aria/utils';
|
||||
import { strictAssert } from '../../../util/assert';
|
||||
|
||||
export type FunTooltipProps = Readonly<{
|
||||
placement?: Placement;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
disableHoverableContent?: boolean;
|
||||
side?: Tooltip.TooltipContentProps['side'];
|
||||
align?: Tooltip.TooltipContentProps['align'];
|
||||
collisionBoundarySelector?: string;
|
||||
collisionPadding?: number;
|
||||
content: ReactNode;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunTooltip(props: FunTooltipProps): JSX.Element {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const [collisionBoundary, setCollisionBoundary] = useState<Element | null>(
|
||||
null
|
||||
);
|
||||
useLayoutEffect(() => {
|
||||
if (props.collisionBoundarySelector == null) {
|
||||
return;
|
||||
}
|
||||
strictAssert(ref.current, 'missing ref');
|
||||
const trigger = ref.current;
|
||||
setCollisionBoundary(trigger.closest(props.collisionBoundarySelector));
|
||||
}, [props.collisionBoundarySelector]);
|
||||
|
||||
return (
|
||||
<Tooltip className="FunTooltip" placement={props.placement}>
|
||||
{props.children}
|
||||
</Tooltip>
|
||||
<Tooltip.Root
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
disableHoverableContent={props.disableHoverableContent}
|
||||
>
|
||||
<Tooltip.Trigger ref={ref} asChild>
|
||||
{props.children}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
className="FunTooltip"
|
||||
collisionBoundary={collisionBoundary}
|
||||
collisionPadding={props.collisionPadding}
|
||||
>
|
||||
<span className="FunTooltip__Text">{props.content}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { MouseEvent, PointerEvent } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Heading,
|
||||
OverlayArrow,
|
||||
Popover,
|
||||
TooltipTrigger,
|
||||
} from 'react-aria-components';
|
||||
import type { PressEvent } from 'react-aria';
|
||||
import { VisuallyHidden } from 'react-aria';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import type { LocalizerType } from '../../../types/I18N';
|
||||
import { strictAssert } from '../../../util/assert';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
@@ -388,95 +388,99 @@ export function FunPanelEmojis({
|
||||
</FunPanelFooter>
|
||||
)}
|
||||
<FunPanelBody>
|
||||
<FunScroller
|
||||
ref={scrollerRef}
|
||||
sectionGap={EMOJI_GRID_SECTION_GAP}
|
||||
onScrollSectionChange={handleScrollSectionChange}
|
||||
>
|
||||
{layout.sections.length === 0 && (
|
||||
<FunResults aria-busy={false}>
|
||||
<FunResultsHeader>
|
||||
{i18n('icu:FunPanelEmojis__SearchResults__EmptyHeading')}{' '}
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={emojiVariantConstant('\u{1F641}')}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
</FunResults>
|
||||
)}
|
||||
{layout.sections.length > 0 && (
|
||||
<FunKeyboard
|
||||
scrollerRef={scrollerRef}
|
||||
keyboard={keyboard}
|
||||
onStateChange={handleKeyboardStateChange}
|
||||
>
|
||||
<FunGridContainer
|
||||
totalSize={layout.totalHeight}
|
||||
columnCount={EMOJI_GRID_COLUMNS}
|
||||
cellWidth={EMOJI_GRID_CELL_WIDTH}
|
||||
cellHeight={EMOJI_GRID_CELL_HEIGHT}
|
||||
<Tooltip.Provider skipDelayDuration={0}>
|
||||
<FunScroller
|
||||
ref={scrollerRef}
|
||||
sectionGap={EMOJI_GRID_SECTION_GAP}
|
||||
onScrollSectionChange={handleScrollSectionChange}
|
||||
>
|
||||
{layout.sections.length === 0 && (
|
||||
<FunResults aria-busy={false}>
|
||||
<FunResultsHeader>
|
||||
{i18n('icu:FunPanelEmojis__SearchResults__EmptyHeading')}{' '}
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={emojiVariantConstant('\u{1F641}')}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
</FunResults>
|
||||
)}
|
||||
{layout.sections.length > 0 && (
|
||||
<FunKeyboard
|
||||
scrollerRef={scrollerRef}
|
||||
keyboard={keyboard}
|
||||
onStateChange={handleKeyboardStateChange}
|
||||
>
|
||||
{layout.sections.map(section => {
|
||||
return (
|
||||
<FunGridScrollerSection
|
||||
key={section.key}
|
||||
id={section.id}
|
||||
sectionOffset={section.sectionOffset}
|
||||
sectionSize={section.sectionSize}
|
||||
>
|
||||
<FunGridHeader
|
||||
id={section.header.key}
|
||||
headerOffset={section.header.headerOffset}
|
||||
headerSize={section.header.headerSize}
|
||||
<FunGridContainer
|
||||
totalSize={layout.totalHeight}
|
||||
columnCount={EMOJI_GRID_COLUMNS}
|
||||
cellWidth={EMOJI_GRID_CELL_WIDTH}
|
||||
cellHeight={EMOJI_GRID_CELL_HEIGHT}
|
||||
>
|
||||
{layout.sections.map(section => {
|
||||
return (
|
||||
<FunGridScrollerSection
|
||||
key={section.key}
|
||||
id={section.id}
|
||||
sectionOffset={section.sectionOffset}
|
||||
sectionSize={section.sectionSize}
|
||||
>
|
||||
<FunGridHeaderText>
|
||||
{getTitleForSection(
|
||||
i18n,
|
||||
section.id as FunEmojisSection
|
||||
)}
|
||||
</FunGridHeaderText>
|
||||
{section.id ===
|
||||
EmojiPickerCategory.SmileysAndPeople && (
|
||||
<SectionSkinToneHeaderPopover
|
||||
i18n={i18n}
|
||||
open={skinTonePopoverOpen}
|
||||
onOpenChange={handleSkinTonePopoverOpenChange}
|
||||
onSelectSkinTone={fun.onEmojiSkinToneDefaultChange}
|
||||
/>
|
||||
)}
|
||||
</FunGridHeader>
|
||||
<FunGridRowGroup
|
||||
aria-labelledby={section.header.key}
|
||||
colCount={section.colCount}
|
||||
rowCount={section.rowCount}
|
||||
rowGroupOffset={section.rowGroup.rowGroupOffset}
|
||||
rowGroupSize={section.rowGroup.rowGroupSize}
|
||||
>
|
||||
{section.rowGroup.rows.map(row => {
|
||||
return (
|
||||
<Row
|
||||
key={row.key}
|
||||
<FunGridHeader
|
||||
id={section.header.key}
|
||||
headerOffset={section.header.headerOffset}
|
||||
headerSize={section.header.headerSize}
|
||||
>
|
||||
<FunGridHeaderText>
|
||||
{getTitleForSection(
|
||||
i18n,
|
||||
section.id as FunEmojisSection
|
||||
)}
|
||||
</FunGridHeaderText>
|
||||
{section.id ===
|
||||
EmojiPickerCategory.SmileysAndPeople && (
|
||||
<SectionSkinToneHeaderPopover
|
||||
i18n={i18n}
|
||||
rowIndex={row.rowIndex}
|
||||
cells={row.cells}
|
||||
focusedCellKey={focusedCellKey}
|
||||
emojiSkinToneDefault={fun.emojiSkinToneDefault}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
onEmojiSkinToneDefaultChange={
|
||||
open={skinTonePopoverOpen}
|
||||
onOpenChange={handleSkinTonePopoverOpenChange}
|
||||
onSelectSkinTone={
|
||||
fun.onEmojiSkinToneDefaultChange
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</FunGridRowGroup>
|
||||
</FunGridScrollerSection>
|
||||
);
|
||||
})}
|
||||
</FunGridContainer>
|
||||
</FunKeyboard>
|
||||
)}
|
||||
</FunScroller>
|
||||
)}
|
||||
</FunGridHeader>
|
||||
<FunGridRowGroup
|
||||
aria-labelledby={section.header.key}
|
||||
colCount={section.colCount}
|
||||
rowCount={section.rowCount}
|
||||
rowGroupOffset={section.rowGroup.rowGroupOffset}
|
||||
rowGroupSize={section.rowGroup.rowGroupSize}
|
||||
>
|
||||
{section.rowGroup.rows.map(row => {
|
||||
return (
|
||||
<Row
|
||||
key={row.key}
|
||||
i18n={i18n}
|
||||
rowIndex={row.rowIndex}
|
||||
cells={row.cells}
|
||||
focusedCellKey={focusedCellKey}
|
||||
emojiSkinToneDefault={fun.emojiSkinToneDefault}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
onEmojiSkinToneDefaultChange={
|
||||
fun.onEmojiSkinToneDefaultChange
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</FunGridRowGroup>
|
||||
</FunGridScrollerSection>
|
||||
);
|
||||
})}
|
||||
</FunGridContainer>
|
||||
</FunKeyboard>
|
||||
)}
|
||||
</FunScroller>
|
||||
</Tooltip.Provider>
|
||||
</FunPanelBody>
|
||||
</FunPanel>
|
||||
);
|
||||
@@ -572,8 +576,8 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
return getEmojiVariantByParentKeyAndSkinTone(emojiParent.key, skinTone);
|
||||
}, [emojiParent, skinTone]);
|
||||
|
||||
const handlePress = useCallback(
|
||||
(event: PressEvent) => {
|
||||
const handleClick = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) {
|
||||
setPopoverOpen(true);
|
||||
return;
|
||||
@@ -585,7 +589,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
skinTone,
|
||||
};
|
||||
const shouldClose =
|
||||
(event.pointerType === 'keyboard' || event.pointerType === 'virtual') &&
|
||||
event.nativeEvent.pointerType !== 'mouse' &&
|
||||
!(event.ctrlKey || event.metaKey);
|
||||
onSelectEmoji(emojiSelection, shouldClose);
|
||||
},
|
||||
@@ -654,12 +658,20 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
colIndex={props.colIndex}
|
||||
rowIndex={props.rowIndex}
|
||||
>
|
||||
<TooltipTrigger>
|
||||
<FunTooltip
|
||||
side="top"
|
||||
content={`:${emojiShortNameDisplay}:`}
|
||||
collisionBoundarySelector=".FunScroller__Viewport"
|
||||
collisionPadding={6}
|
||||
// `skipDelayDuration=0` doesn't work with `disableHoverableContent`
|
||||
// FIX: https://github.com/radix-ui/primitives/pull/3562
|
||||
// disableHoverableContent
|
||||
>
|
||||
<FunItemButton
|
||||
ref={popoverTriggerRef}
|
||||
excludeFromTabOrder={!props.isTabbable}
|
||||
aria-label={emojiName}
|
||||
onPress={handlePress}
|
||||
onClick={handleClick}
|
||||
onLongPress={handleLongPress}
|
||||
onContextMenu={handleContextMenu}
|
||||
longPressAccessibilityDescription={i18n(
|
||||
@@ -668,9 +680,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
>
|
||||
<FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} />
|
||||
</FunItemButton>
|
||||
<FunTooltip placement="top">{`:${emojiShortNameDisplay}:`}</FunTooltip>
|
||||
</TooltipTrigger>
|
||||
d
|
||||
</FunTooltip>
|
||||
{emojiHasSkinToneVariants && (
|
||||
<Popover
|
||||
data-fun-overlay
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { Range } from '@tanstack/react-virtual';
|
||||
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual';
|
||||
import type { PointerEvent } from 'react';
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
@@ -11,7 +12,6 @@ import React, {
|
||||
useState,
|
||||
useId,
|
||||
} from 'react';
|
||||
import type { PressEvent } from 'react-aria';
|
||||
import { VisuallyHidden } from 'react-aria';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { FunItemButton } from '../base/FunItem';
|
||||
@@ -356,8 +356,8 @@ export function FunPanelGifs({
|
||||
[]
|
||||
);
|
||||
|
||||
const handlePressGif = useCallback(
|
||||
(_event: PressEvent, gifSelection: FunGifSelection) => {
|
||||
const handleClickGif = useCallback(
|
||||
(_event: PointerEvent, gifSelection: FunGifSelection) => {
|
||||
onFunSelectGif(gifSelection);
|
||||
onSelectGif(gifSelection);
|
||||
setSelectedItemKey(null);
|
||||
@@ -520,7 +520,7 @@ export function FunPanelGifs({
|
||||
itemOffset={item.start}
|
||||
itemLane={item.lane}
|
||||
isTabbable={isTabbable}
|
||||
onPressGif={handlePressGif}
|
||||
onClickGif={handleClickGif}
|
||||
fetchGif={fetchGif}
|
||||
/>
|
||||
);
|
||||
@@ -542,16 +542,16 @@ const Item = memo(function Item(props: {
|
||||
itemOffset: number;
|
||||
itemLane: number;
|
||||
isTabbable: boolean;
|
||||
onPressGif: (event: PressEvent, gifSelection: FunGifSelection) => void;
|
||||
onClickGif: (event: PointerEvent, gifSelection: FunGifSelection) => void;
|
||||
fetchGif: typeof tenorDownload;
|
||||
}) {
|
||||
const { onPressGif, fetchGif } = props;
|
||||
const { onClickGif, fetchGif } = props;
|
||||
|
||||
const handlePress = useCallback(
|
||||
async (event: PressEvent) => {
|
||||
onPressGif(event, { gif: props.gif });
|
||||
const handleClick = useCallback(
|
||||
async (event: PointerEvent) => {
|
||||
onClickGif(event, { gif: props.gif });
|
||||
},
|
||||
[props.gif, onPressGif]
|
||||
[props.gif, onClickGif]
|
||||
);
|
||||
|
||||
const descriptionId = `FunGifsPanelItem__GifDescription--${props.gif.id}`;
|
||||
@@ -606,7 +606,7 @@ const Item = memo(function Item(props: {
|
||||
>
|
||||
<FunItemButton
|
||||
aria-label={props.gif.title}
|
||||
onPress={handlePress}
|
||||
onClick={handleClick}
|
||||
excludeFromTabOrder={!props.isTabbable}
|
||||
>
|
||||
{src != null && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CSSProperties, PointerEvent } from 'react';
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
@@ -9,7 +9,6 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { PressEvent } from 'react-aria';
|
||||
import type {
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
@@ -344,8 +343,8 @@ export function FunPanelStickers({
|
||||
return searchInput.length > 0;
|
||||
}, [searchInput]);
|
||||
|
||||
const handlePressSticker = useCallback(
|
||||
(event: PressEvent, stickerSelection: FunStickerSelection) => {
|
||||
const handleClickSticker = useCallback(
|
||||
(event: PointerEvent, stickerSelection: FunStickerSelection) => {
|
||||
onFunSelectSticker(stickerSelection);
|
||||
onSelectSticker(stickerSelection);
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
@@ -356,8 +355,8 @@ export function FunPanelStickers({
|
||||
[onFunSelectSticker, onSelectSticker, onClose]
|
||||
);
|
||||
|
||||
const handlePressTimeSticker = useCallback(
|
||||
(event: PressEvent, style: FunTimeStickerStyle) => {
|
||||
const handleClickTimeSticker = useCallback(
|
||||
(event: PointerEvent, style: FunTimeStickerStyle) => {
|
||||
onSelectTimeSticker?.(style);
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
onClose();
|
||||
@@ -492,8 +491,8 @@ export function FunPanelStickers({
|
||||
cells={row.cells}
|
||||
stickerLookup={stickerLookup}
|
||||
focusedCellKey={focusedCellKey}
|
||||
onPressSticker={handlePressSticker}
|
||||
onPressTimeSticker={handlePressTimeSticker}
|
||||
onClickSticker={handleClickSticker}
|
||||
onClickTimeSticker={handleClickTimeSticker}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -515,11 +514,11 @@ const Row = memo(function Row(props: {
|
||||
stickerLookup: StickerLookup;
|
||||
cells: ReadonlyArray<CellLayoutNode>;
|
||||
focusedCellKey: CellKey | null;
|
||||
onPressSticker: (
|
||||
event: PressEvent,
|
||||
onClickSticker: (
|
||||
event: PointerEvent,
|
||||
stickerSelection: FunStickerSelection
|
||||
) => void;
|
||||
onPressTimeSticker: (event: PressEvent, style: FunTimeStickerStyle) => void;
|
||||
onClickTimeSticker: (event: PointerEvent, style: FunTimeStickerStyle) => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<FunGridRow rowIndex={props.rowIndex}>
|
||||
@@ -537,8 +536,8 @@ const Row = memo(function Row(props: {
|
||||
colIndex={cell.colIndex}
|
||||
stickerLookup={props.stickerLookup}
|
||||
isTabbable={isTabbable}
|
||||
onPressSticker={props.onPressSticker}
|
||||
onPressTimeSticker={props.onPressTimeSticker}
|
||||
onClickSticker={props.onClickSticker}
|
||||
onClickTimeSticker={props.onClickTimeSticker}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -553,28 +552,28 @@ const Cell = memo(function Cell(props: {
|
||||
rowIndex: number;
|
||||
stickerLookup: StickerLookup;
|
||||
isTabbable: boolean;
|
||||
onPressSticker: (
|
||||
event: PressEvent,
|
||||
onClickSticker: (
|
||||
event: PointerEvent,
|
||||
stickerSelection: FunStickerSelection
|
||||
) => void;
|
||||
onPressTimeSticker: (event: PressEvent, style: FunTimeStickerStyle) => void;
|
||||
onClickTimeSticker: (event: PointerEvent, style: FunTimeStickerStyle) => void;
|
||||
}): JSX.Element {
|
||||
const { onPressSticker, onPressTimeSticker } = props;
|
||||
const { onClickSticker, onClickTimeSticker } = props;
|
||||
const stickerLookupItem = props.stickerLookup[props.value];
|
||||
|
||||
const handlePress = useCallback(
|
||||
(event: PressEvent) => {
|
||||
const handleClick = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (stickerLookupItem.kind === 'sticker') {
|
||||
onPressSticker(event, {
|
||||
onClickSticker(event, {
|
||||
stickerPackId: stickerLookupItem.sticker.packId,
|
||||
stickerId: stickerLookupItem.sticker.id,
|
||||
stickerUrl: stickerLookupItem.sticker.url,
|
||||
});
|
||||
} else if (stickerLookupItem.kind === 'timeSticker') {
|
||||
onPressTimeSticker(event, stickerLookupItem.style);
|
||||
onClickTimeSticker(event, stickerLookupItem.style);
|
||||
}
|
||||
},
|
||||
[stickerLookupItem, onPressSticker, onPressTimeSticker]
|
||||
[stickerLookupItem, onClickSticker, onClickTimeSticker]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -590,7 +589,7 @@ const Cell = memo(function Cell(props: {
|
||||
? (stickerLookupItem.sticker.emoji ?? '')
|
||||
: stickerLookupItem.style
|
||||
}
|
||||
onPress={handlePress}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{stickerLookupItem.kind === 'sticker' && (
|
||||
<FunSticker
|
||||
|
||||
Reference in New Issue
Block a user