mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-24 20:26:24 +00:00
Add tooltips to fun picker
This commit is contained in:
@@ -21,4 +21,5 @@
|
||||
@use './FunSticker.scss';
|
||||
@use './FunSubNav.scss';
|
||||
@use './FunTabs.scss';
|
||||
@use './FunTooltip.scss';
|
||||
@use './FunWaterfall.scss';
|
||||
|
||||
27
stylesheets/components/fun/FunTooltip.scss
Normal file
27
stylesheets/components/fun/FunTooltip.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@use '../../mixins';
|
||||
@use '../../variables';
|
||||
@use './FunConstants.scss';
|
||||
|
||||
.FunTooltip {
|
||||
max-width: calc(32ch + (8px * 2));
|
||||
padding-block: 4px;
|
||||
padding-inline: 8px;
|
||||
background: FunConstants.$Fun__BgColor;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-80,
|
||||
variables.$color-white-alpha-80
|
||||
);
|
||||
border-radius: 6px;
|
||||
box-shadow:
|
||||
0px 4px 12px variables.$color-black-alpha-30,
|
||||
0px 0px 4px variables.$color-black-alpha-05;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
@include mixins.font-body-2();
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ForwardedRef, MouseEvent, ReactNode } from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import {
|
||||
mergeProps,
|
||||
type PressEvent,
|
||||
useLongPress,
|
||||
usePress,
|
||||
} from 'react-aria';
|
||||
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 { strictAssert } from '../../../util/assert';
|
||||
|
||||
/**
|
||||
* Button
|
||||
@@ -28,7 +27,7 @@ export type FunItemButtonLongPressProps = Readonly<
|
||||
export type FunItemButtonProps = Readonly<
|
||||
{
|
||||
'aria-label': string;
|
||||
tabIndex: number;
|
||||
excludeFromTabOrder: boolean;
|
||||
onPress: (event: PressEvent) => void;
|
||||
onContextMenu?: (event: MouseEvent) => void;
|
||||
children: ReactNode;
|
||||
@@ -37,11 +36,10 @@ export type FunItemButtonProps = Readonly<
|
||||
|
||||
export const FunItemButton = forwardRef(function FunItemButton(
|
||||
props: FunItemButtonProps,
|
||||
ref: ForwardedRef<HTMLButtonElement>
|
||||
outerRef: ForwardedRef<HTMLButtonElement>
|
||||
): JSX.Element {
|
||||
const { pressProps } = usePress({
|
||||
onPress: props.onPress,
|
||||
});
|
||||
const { onContextMenu } = props;
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const { longPressProps } = useLongPress({
|
||||
isDisabled: props.onLongPress == null,
|
||||
@@ -49,17 +47,30 @@ export const FunItemButton = forwardRef(function FunItemButton(
|
||||
onLongPress: props.onLongPress,
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className="FunItem__Button"
|
||||
aria-label={props['aria-label']}
|
||||
tabIndex={props.tabIndex}
|
||||
{...mergeProps(pressProps, longPressProps)}
|
||||
onContextMenu={props.onContextMenu}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,6 +64,25 @@ export function FunSubNavScroller(props: FunSubNavScrollerProps): JSX.Element {
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
strictAssert(outerRef.current, 'Must have scroller ref');
|
||||
const scroller = outerRef.current;
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
scroller.scrollBy({
|
||||
left: event.deltaX + event.deltaY,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
|
||||
scroller.addEventListener('wheel', onWheel, { passive: false });
|
||||
|
||||
return () => {
|
||||
scroller.addEventListener('wheel', onWheel, { passive: false });
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="FunSubNav__Scroller">
|
||||
<div
|
||||
@@ -203,12 +222,21 @@ function FunSubNavListBoxItemButton(props: {
|
||||
|
||||
useEffect(() => {
|
||||
strictAssert(ref.current, 'Expected ref to be defined');
|
||||
const element = ref.current;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
if (props.isSelected) {
|
||||
ref.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
});
|
||||
// Needs setTimeout() for arrow key navigation to work.
|
||||
// Might be something to do with native arrow key scroll handling.
|
||||
timer = setTimeout(() => {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}, 1);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [props.isSelected]);
|
||||
|
||||
return (
|
||||
@@ -230,7 +258,7 @@ export function FunSubNavListBoxItem(
|
||||
aria-label={props.label}
|
||||
textValue={props.label}
|
||||
>
|
||||
{({ isSelected }) => {
|
||||
{({ isSelected, isFocusVisible }) => {
|
||||
return (
|
||||
<FunSubNavListBoxItemButton isSelected={isSelected}>
|
||||
<span className="FunSubNav__ListBoxItem__ButtonIcon">
|
||||
@@ -244,6 +272,9 @@ export function FunSubNavListBoxItem(
|
||||
transition={FunSubNavListBoxItemTransition}
|
||||
/>
|
||||
)}
|
||||
{!isSelected && isFocusVisible && (
|
||||
<div className="FunSubNav__ListBoxItem__ButtonIndicator" />
|
||||
)}
|
||||
</FunSubNavListBoxItemButton>
|
||||
);
|
||||
}}
|
||||
|
||||
19
ts/components/fun/base/FunTooltip.tsx
Normal file
19
ts/components/fun/base/FunTooltip.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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';
|
||||
|
||||
export type FunTooltipProps = Readonly<{
|
||||
placement?: Placement;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunTooltip(props: FunTooltipProps): JSX.Element {
|
||||
return (
|
||||
<Tooltip className="FunTooltip" placement={props.placement}>
|
||||
{props.children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -560,7 +560,7 @@ export function emojiVariantConstant(input: string): EmojiVariantData {
|
||||
export function normalizeShortNameCompletionDisplay(shortName: string): string {
|
||||
return removeDiacritics(shortName)
|
||||
.normalize('NFD')
|
||||
.replaceAll(' ', '_')
|
||||
.replaceAll(/[\s,]+/gi, '_')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ export function normalizeShortNameCompletionDisplay(shortName: string): string {
|
||||
export function normalizeShortNameCompletionQuery(query: string): string {
|
||||
return removeDiacritics(query)
|
||||
.normalize('NFD')
|
||||
.replaceAll(/[\s_-]+/gi, ' ')
|
||||
.replaceAll(/[\s,_-]+/gi, ' ')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
Heading,
|
||||
OverlayArrow,
|
||||
Popover,
|
||||
TooltipTrigger,
|
||||
} from 'react-aria-components';
|
||||
import type { PressEvent } from 'react-aria';
|
||||
import { VisuallyHidden } from 'react-aria';
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
getEmojiPickerCategoryParentKeys,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
isEmojiParentKey,
|
||||
normalizeShortNameCompletionDisplay,
|
||||
} from '../data/emojis';
|
||||
import { useFunEmojiSearch } from '../useFunEmojiSearch';
|
||||
import { FunKeyboard } from '../keyboard/FunKeyboard';
|
||||
@@ -70,6 +71,7 @@ import { FunStaticEmoji } from '../FunEmoji';
|
||||
import { useFunContext } from '../FunProvider';
|
||||
import { FunResults, FunResultsHeader } from '../base/FunResults';
|
||||
import { useFunEmojiLocalizer } from '../useFunEmojiLocalizer';
|
||||
import { FunTooltip } from '../base/FunTooltip';
|
||||
|
||||
function getTitleForSection(
|
||||
i18n: LocalizerType,
|
||||
@@ -638,7 +640,13 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
]
|
||||
);
|
||||
|
||||
const emojiName = emojiLocalizer(emojiVariant.key);
|
||||
const emojiName = useMemo(() => {
|
||||
return emojiLocalizer(emojiVariant.key);
|
||||
}, [emojiVariant.key, emojiLocalizer]);
|
||||
|
||||
const emojiShortNameDisplay = useMemo(() => {
|
||||
return normalizeShortNameCompletionDisplay(emojiName);
|
||||
}, [emojiName]);
|
||||
|
||||
return (
|
||||
<FunGridCell
|
||||
@@ -646,19 +654,23 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
colIndex={props.colIndex}
|
||||
rowIndex={props.rowIndex}
|
||||
>
|
||||
<FunItemButton
|
||||
ref={popoverTriggerRef}
|
||||
tabIndex={props.isTabbable ? 0 : -1}
|
||||
aria-label={emojiName}
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
onContextMenu={handleContextMenu}
|
||||
longPressAccessibilityDescription={i18n(
|
||||
'icu:FunPanelEmojis__SkinTonePicker__LongPressAccessibilityDescription'
|
||||
)}
|
||||
>
|
||||
<FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} />
|
||||
</FunItemButton>
|
||||
<TooltipTrigger>
|
||||
<FunItemButton
|
||||
ref={popoverTriggerRef}
|
||||
excludeFromTabOrder={!props.isTabbable}
|
||||
aria-label={emojiName}
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
onContextMenu={handleContextMenu}
|
||||
longPressAccessibilityDescription={i18n(
|
||||
'icu:FunPanelEmojis__SkinTonePicker__LongPressAccessibilityDescription'
|
||||
)}
|
||||
>
|
||||
<FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} />
|
||||
</FunItemButton>
|
||||
<FunTooltip placement="top">{`:${emojiShortNameDisplay}:`}</FunTooltip>
|
||||
</TooltipTrigger>
|
||||
d
|
||||
{emojiHasSkinToneVariants && (
|
||||
<Popover
|
||||
data-fun-overlay
|
||||
|
||||
@@ -606,7 +606,7 @@ const Item = memo(function Item(props: {
|
||||
<FunItemButton
|
||||
aria-label={props.gif.title}
|
||||
onPress={handlePress}
|
||||
tabIndex={props.isTabbable ? 0 : -1}
|
||||
excludeFromTabOrder={!props.isTabbable}
|
||||
>
|
||||
{src != null && (
|
||||
<FunGif
|
||||
|
||||
@@ -584,7 +584,7 @@ const Cell = memo(function Cell(props: {
|
||||
rowIndex={props.rowIndex}
|
||||
>
|
||||
<FunItemButton
|
||||
tabIndex={props.isTabbable ? 0 : -1}
|
||||
excludeFromTabOrder={!props.isTabbable}
|
||||
aria-label={
|
||||
stickerLookupItem.kind === 'sticker'
|
||||
? (stickerLookupItem.sticker.emoji ?? '')
|
||||
|
||||
@@ -2022,6 +2022,13 @@
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-02-19T20:14:46.879Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/fun/base/FunItem.tsx",
|
||||
"line": " const innerRef = useRef<HTMLButtonElement>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-04-23T23:43:10.675Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/fun/base/FunScroller.tsx",
|
||||
|
||||
Reference in New Issue
Block a user