feat(app): gate the expression info card behind an experiment (#2773)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-14 20:14:58 +02:00
1 parent 7c9274847f
commit 6339c3b8ad
110 files changed
+12827 -5191

No files matched your search

@@ -20,8 +20,13 @@
}
.emoji {
display: block;
flex-shrink: 0;
width: 2rem;
height: 2rem;
min-width: 2rem;
min-height: 2rem;
object-fit: contain;
}
.textContainer {
@@ -50,7 +50,7 @@ export const EmojiTooltipContent = React.forwardRef<HTMLDivElement, EmojiTooltip
src={emojiUrl}
alt={emojiAlt}
draggable={false}
className={clsx('emoji', styles.emoji, 'jumboable', emojiClassName)}
className={clsx(styles.emoji, emojiClassName)}
data-flx="ui.emoji-tooltip-content.emoji-tooltip-content.render-emoji.emoji"
/>
);
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {isPopoutDragClick} from '@app/features/ui/popover/PopoverDragClickUtils';
import {describe, expect, it} from 'vitest';
const PRESS = {x: 120, y: 240, button: 0};
describe('isPopoutDragClick', () => {
it('treats a click far from the press as a drag', () => {
expect(isPopoutDragClick(PRESS, {detail: 1, button: 0, clientX: 160, clientY: 240})).toBe(true);
});
it('allows a click within the tolerance of the press', () => {
expect(isPopoutDragClick(PRESS, {detail: 1, button: 0, clientX: 122, clientY: 242})).toBe(false);
});
it('allows a click from a different button than the press', () => {
expect(isPopoutDragClick(PRESS, {detail: 1, button: 2, clientX: 900, clientY: 900})).toBe(false);
});
it('allows a click with no recorded press', () => {
expect(isPopoutDragClick(null, {detail: 1, button: 0, clientX: 900, clientY: 900})).toBe(false);
});
it('allows a keyboard click even when a stale press is recorded', () => {
expect(isPopoutDragClick(PRESS, {detail: 0, button: 0, clientX: 0, clientY: 0})).toBe(false);
});
});
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const CLICK_DRAG_TOLERANCE_PX = 3;
export interface PopoutClickStart {
x: number;
y: number;
button: number;
}
interface PopoutClickGeometry {
detail: number;
button: number;
clientX: number;
clientY: number;
}
export function isPopoutDragClick(clickStart: PopoutClickStart | null, event: PopoutClickGeometry): boolean {
if (event.detail === 0) {
return false;
}
if (!clickStart || clickStart.button !== event.button) {
return false;
}
return (
Math.abs(event.clientX - clickStart.x) > CLICK_DRAG_TOLERANCE_PX ||
Math.abs(event.clientY - clickStart.y) > CLICK_DRAG_TOLERANCE_PX
);
}
@@ -12,6 +12,7 @@ import {
type PopoutReferenceRect,
usePopoutKeyContext,
} from '@app/features/ui/popover';
import {isPopoutDragClick, type PopoutClickStart} from '@app/features/ui/popover/PopoverDragClickUtils';
import styles from '@app/features/ui/popover/PopoverPopout.module.css';
import {schedulePopoutPortalCleanup} from '@app/features/ui/popover/PopoverPortalCleanup';
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
@@ -27,7 +28,6 @@ import {autorun} from 'mobx';
import React, {useCallback, useEffect, useRef, useState} from 'react';
let currentId = 1;
const CLICK_DRAG_TOLERANCE_PX = 3;
const MENU_ITEM_SELECTOR =
'[data-roving-focus="true"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
@@ -68,12 +68,6 @@ interface OpenPopoutOptions extends Partial<PopoutProps> {
onContentMouseLeave?: () => void;
}
interface ClickStart {
x: number;
y: number;
button: number;
}
type PopoutOpenMode = 'click' | 'hover';
export const openPopout = (target: HTMLElement, props: OpenPopoutOptions, key: string | number, clickPos = 0) => {
@@ -190,7 +184,7 @@ export const Popout = React.forwardRef<HTMLElement, PopoutProps>((props, ref) =>
const isContentHoveringRef = useRef(false);
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
const closeTimerRef = useRef<NodeJS.Timeout | null>(null);
const clickStartRef = useRef<ClickStart | null>(null);
const clickStartRef = useRef<PopoutClickStart | null>(null);
const openModeRef = useRef<PopoutOpenMode | null>(null);
useEffect(() => {
if (props.children) {
@@ -454,13 +448,7 @@ export const Popout = React.forwardRef<HTMLElement, PopoutProps>((props, ref) =>
const consumeDragClick = (event: React.MouseEvent<HTMLElement>) => {
const clickStart = clickStartRef.current;
clickStartRef.current = null;
if (!clickStart || clickStart.button !== event.button) {
return false;
}
return (
Math.abs(event.clientX - clickStart.x) > CLICK_DRAG_TOLERANCE_PX ||
Math.abs(event.clientY - clickStart.y) > CLICK_DRAG_TOLERANCE_PX
);
return isPopoutDragClick(clickStart, event);
};
const enhancedChild = React.cloneElement(child, {
...popoutTriggerProps,
@@ -10,6 +10,8 @@ import {PopoutResizePositionContext} from '@app/features/ui/popover/PopoutResize
import {getPopoutFocusManagerInsideElements} from '@app/features/ui/popover/PopoverFocusManagerUtils';
import styles from '@app/features/ui/popover/PopoverPopout.module.css';
import {scheduleFloatingPortalSweep} from '@app/features/ui/popover/PopoverPortalCleanup';
import {observePopoutKeyboardDismiss, resolvePopoutReturnFocus} from '@app/features/ui/popover/PopoverReturnFocusUtils';
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
import LayerManager from '@app/features/ui/state/LayerManager';
import PopoutState from '@app/features/ui/state/Popout';
import {isScrollbarDragActive} from '@app/features/ui/utils/ScrollbarDragState';
@@ -209,9 +211,11 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
}, [focusRefs, target]);
const mergedPopoutRef = useMergeRefs([setFloating, focusRefs.setFloating]);
const prefersReducedMotion = Accessibility.useReducedMotion;
const isKeyboardModeEnabled = KeyboardMode.keyboardModeEnabled;
const [isVisible, setIsVisible] = useState(true);
const [targetInDOM, setTargetInDOM] = useState(() => ownerDocument.contains(target));
const hasFocusedInitialRef = useRef(false);
const keyboardDismissReturnRef = useRef<HTMLElement | null>(null);
const closeTimerRef = useRef<number | null>(null);
const beginClose = useCallback(() => {
if (closeTimerRef.current != null) return;
@@ -290,6 +294,22 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
if (returnFocusRef != null) returnFocusElement = returnFocusRef.current;
return getPopoutFocusManagerInsideElements(target, returnFocusElement);
}, [target, returnFocusRef]);
const resolveKeyboardDismissFocusTarget = useCallback((): HTMLElement | null => {
if (!LayerManager.isTopLayer('popout', popoutKey)) {
return null;
}
return returnFocusRef?.current ?? target;
}, [popoutKey, returnFocusRef, target]);
useEffect(() => {
if (ownerWindow == null) {
return;
}
return observePopoutKeyboardDismiss({
ownerWindow,
keyboardDismissRef: keyboardDismissReturnRef,
resolveFocusTarget: resolveKeyboardDismissFocusTarget,
});
}, [ownerWindow, resolveKeyboardDismissFocusTarget]);
useEffect(() => {
const el = popoutRef.current;
const targetIsConnected = ownerDocument.contains(target);
@@ -378,7 +398,13 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
<FloatingFocusManager
context={focusContext}
disabled={!isTopmost}
returnFocus={returnFocusOnClose ? (returnFocusRef == null ? targetInDOM : returnFocusRef) : false}
returnFocus={resolvePopoutReturnFocus({
restoreFocusPolicy: returnFocusOnClose,
isKeyboardModeEnabled,
returnFocusRef,
keyboardDismissRef: keyboardDismissReturnRef,
isTargetInDOM: targetInDOM,
})}
initialFocus={focusRefs.floating}
getInsideElements={getFocusManagerInsideElements}
data-flx="ui.popover.popouts.popout-item.floating-focus-manager"
@@ -0,0 +1,274 @@
// @vitest-environment happy-dom
// SPDX-License-Identifier: AGPL-3.0-or-later
import {readFileSync} from 'node:fs';
import {resolve} from 'node:path';
import {observePopoutKeyboardDismiss, resolvePopoutReturnFocus} from '@app/features/ui/popover/PopoverReturnFocusUtils';
import {FloatingFocusManager, useFloating} from '@floating-ui/react';
import {act, createElement, useLayoutEffect} from 'react';
import {createRoot, type Root} from 'react-dom/client';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
const RETURN_FOCUS_REF = {current: null};
const KEYBOARD_DISMISS_REF = {current: null};
describe('resolvePopoutReturnFocus', () => {
it('returns focus to the target when the open-time policy allows it', () => {
expect(
resolvePopoutReturnFocus({
restoreFocusPolicy: true,
isKeyboardModeEnabled: false,
keyboardDismissRef: KEYBOARD_DISMISS_REF,
isTargetInDOM: true,
}),
).toBe(true);
});
it('returns focus to the target when the viewer is navigating by keyboard', () => {
expect(
resolvePopoutReturnFocus({
restoreFocusPolicy: false,
isKeyboardModeEnabled: true,
keyboardDismissRef: KEYBOARD_DISMISS_REF,
isTargetInDOM: true,
}),
).toBe(true);
});
it('defers a pointer-driven popout to the late-bound keyboard-dismiss target', () => {
expect(
resolvePopoutReturnFocus({
restoreFocusPolicy: false,
isKeyboardModeEnabled: false,
keyboardDismissRef: KEYBOARD_DISMISS_REF,
isTargetInDOM: true,
}),
).toBe(KEYBOARD_DISMISS_REF);
});
it('prefers an explicit return-focus target over the popout target', () => {
expect(
resolvePopoutReturnFocus({
restoreFocusPolicy: false,
isKeyboardModeEnabled: true,
returnFocusRef: RETURN_FOCUS_REF,
keyboardDismissRef: KEYBOARD_DISMISS_REF,
isTargetInDOM: true,
}),
).toBe(RETURN_FOCUS_REF);
});
it('defers an explicit return-focus target to the keyboard-dismiss target when no return is wanted', () => {
expect(
resolvePopoutReturnFocus({
restoreFocusPolicy: false,
isKeyboardModeEnabled: false,
returnFocusRef: RETURN_FOCUS_REF,
keyboardDismissRef: KEYBOARD_DISMISS_REF,
isTargetInDOM: true,
}),
).toBe(KEYBOARD_DISMISS_REF);
});
it('does not return focus to a target that left the document', () => {
expect(
resolvePopoutReturnFocus({
restoreFocusPolicy: true,
isKeyboardModeEnabled: true,
keyboardDismissRef: KEYBOARD_DISMISS_REF,
isTargetInDOM: false,
}),
).toBe(false);
});
});
describe('observePopoutKeyboardDismiss', () => {
let trigger: HTMLElement;
let keyboardDismissRef: {current: HTMLElement | null};
let dispose: (() => void) | null = null;
let layerManagerStub: ((event: KeyboardEvent) => void) | null = null;
const pressKey = (key: string): void => {
document.body.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true, cancelable: true}));
};
beforeEach(() => {
trigger = document.createElement('button');
document.body.append(trigger);
keyboardDismissRef = {current: null};
});
afterEach(() => {
dispose?.();
dispose = null;
if (layerManagerStub) {
document.removeEventListener('keydown', layerManagerStub, {capture: true});
layerManagerStub = null;
}
document.body.replaceChildren();
});
it('records the focus target when Escape is pressed', () => {
dispose = observePopoutKeyboardDismiss({
ownerWindow: window,
keyboardDismissRef,
resolveFocusTarget: () => trigger,
});
pressKey('Escape');
expect(keyboardDismissRef.current).toBe(trigger);
});
it('records the focus target even though the layer manager stops immediate propagation first', () => {
let didCloseTopLayer = false;
layerManagerStub = (event: KeyboardEvent) => {
if (event.key !== 'Escape') {
return;
}
didCloseTopLayer = true;
event.preventDefault();
event.stopImmediatePropagation();
};
document.addEventListener('keydown', layerManagerStub, {capture: true});
dispose = observePopoutKeyboardDismiss({
ownerWindow: window,
keyboardDismissRef,
resolveFocusTarget: () => trigger,
});
pressKey('Escape');
expect(didCloseTopLayer).toBe(true);
expect(keyboardDismissRef.current).toBe(trigger);
});
it('ignores keys other than Escape', () => {
dispose = observePopoutKeyboardDismiss({
ownerWindow: window,
keyboardDismissRef,
resolveFocusTarget: () => trigger,
});
pressKey('Enter');
pressKey('Tab');
expect(keyboardDismissRef.current).toBeNull();
});
it('records nothing when the popout is not the layer Escape closes', () => {
dispose = observePopoutKeyboardDismiss({
ownerWindow: window,
keyboardDismissRef,
resolveFocusTarget: () => null,
});
pressKey('Escape');
expect(keyboardDismissRef.current).toBeNull();
});
it('stops recording once disposed', () => {
const stop = observePopoutKeyboardDismiss({
ownerWindow: window,
keyboardDismissRef,
resolveFocusTarget: () => trigger,
});
stop();
pressKey('Escape');
expect(keyboardDismissRef.current).toBeNull();
});
});
describe('FloatingFocusManager return-focus ref contract', () => {
let host: HTMLDivElement;
let trigger: HTMLButtonElement;
let root: Root;
function ReturnFocusHarness({
reference,
returnFocus,
}: {
reference: HTMLElement;
returnFocus: {current: HTMLElement | null};
}) {
const {refs, context} = useFloating({open: true});
useLayoutEffect(() => {
refs.setReference(reference);
}, [refs, reference]);
return createElement(FloatingFocusManager, {
context,
returnFocus,
initialFocus: refs.floating,
children: createElement('div', {ref: refs.setFloating, tabIndex: -1, 'data-testid': 'floating'}),
});
}
async function settle(): Promise<void> {
for (let attempt = 0; attempt < 10; attempt += 1) {
await act(async () => {
await Promise.resolve();
});
}
}
beforeEach(() => {
host = document.createElement('div');
trigger = document.createElement('button');
document.body.append(host, trigger);
root = createRoot(host);
});
afterEach(() => {
document.body.replaceChildren();
});
it('focuses a return target assigned after the popout opened', async () => {
const returnFocus: {current: HTMLElement | null} = {current: null};
await act(async () => {
root.render(createElement(ReturnFocusHarness, {reference: trigger, returnFocus}));
});
await settle();
host.querySelector<HTMLElement>('[data-testid="floating"]')?.focus();
returnFocus.current = trigger;
await act(async () => {
root.unmount();
});
await settle();
expect(document.activeElement).toBe(trigger);
});
it('leaves focus alone when no return target was assigned', async () => {
const returnFocus: {current: HTMLElement | null} = {current: null};
await act(async () => {
root.render(createElement(ReturnFocusHarness, {reference: trigger, returnFocus}));
});
await settle();
host.querySelector<HTMLElement>('[data-testid="floating"]')?.focus();
await act(async () => {
root.unmount();
});
await settle();
expect(document.activeElement).not.toBe(trigger);
});
});
describe('PopoverPopouts keyboard-dismiss wiring', () => {
const source = readFileSync(resolve(process.cwd(), 'src/features/ui/popover/PopoverPopouts.tsx'), 'utf8');
it('shares one ref between the Escape watcher and the return-focus resolver', () => {
expect(source.split('keyboardDismissRef: keyboardDismissReturnRef').length - 1).toBe(2);
expect(source).toContain('returnFocus={resolvePopoutReturnFocus({');
expect(source).toContain('resolveFocusTarget: resolveKeyboardDismissFocusTarget');
});
it('only records Escape for the layer the layer manager would close', () => {
expect(source).toContain("LayerManager.isTopLayer('popout', popoutKey)");
});
});
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
interface PopoutReturnFocusInput<TReturnFocusRef> {
restoreFocusPolicy: boolean;
isKeyboardModeEnabled: boolean;
returnFocusRef?: TReturnFocusRef | null;
keyboardDismissRef: TReturnFocusRef;
isTargetInDOM: boolean;
}
export function resolvePopoutReturnFocus<TReturnFocusRef>({
restoreFocusPolicy,
isKeyboardModeEnabled,
returnFocusRef,
keyboardDismissRef,
isTargetInDOM,
}: PopoutReturnFocusInput<TReturnFocusRef>): boolean | TReturnFocusRef {
if (!restoreFocusPolicy && !isKeyboardModeEnabled) {
return keyboardDismissRef;
}
if (returnFocusRef != null) {
return returnFocusRef;
}
return isTargetInDOM;
}
interface PopoutKeyboardDismissInput {
ownerWindow: Window;
keyboardDismissRef: {current: HTMLElement | null};
resolveFocusTarget: () => HTMLElement | null;
}
export function observePopoutKeyboardDismiss({
ownerWindow,
keyboardDismissRef,
resolveFocusTarget,
}: PopoutKeyboardDismissInput): () => void {
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key !== 'Escape') {
return;
}
keyboardDismissRef.current = resolveFocusTarget();
};
ownerWindow.addEventListener('keydown', handleKeyDown, true);
return () => {
ownerWindow.removeEventListener('keydown', handleKeyDown, true);
};
}