Fun picker improvements
This commit is contained in:
parent
427f91f903
commit
b0653d06fe
142 changed files with 3581 additions and 1280 deletions
158
ts/components/fun/base/FunLightbox.tsx
Normal file
158
ts/components/fun/base/FunLightbox.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { strictAssert } from '../../../util/assert';
|
||||
|
||||
/**
|
||||
* Tracks the current `data-key` that has a long-press/long-focus
|
||||
*/
|
||||
const FunLightboxKeyContext = createContext<string | null>(null);
|
||||
|
||||
export function useFunLightboxKey(): string | null {
|
||||
return useContext(FunLightboxKeyContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider
|
||||
*/
|
||||
|
||||
export type FunLightboxProviderProps = Readonly<{
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunLightboxProvider(
|
||||
props: FunLightboxProviderProps
|
||||
): JSX.Element {
|
||||
const [lightboxKey, setLightboxKey] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
strictAssert(props.containerRef.current, 'Missing container ref');
|
||||
const container = props.containerRef.current;
|
||||
|
||||
let isLongPressed = false;
|
||||
let lastLongPress: number | null = null;
|
||||
let currentKey: string | null;
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
|
||||
function lookupKey(event: Event): string | null {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
const closest = event.target.closest('[data-key]');
|
||||
if (!(closest instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
const { key } = closest.dataset;
|
||||
strictAssert(key, 'Must have key');
|
||||
return key;
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (isLongPressed && currentKey != null) {
|
||||
setLightboxKey(currentKey);
|
||||
} else {
|
||||
setLightboxKey(null);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDown(event: MouseEvent) {
|
||||
currentKey = lookupKey(event);
|
||||
timer = setTimeout(() => {
|
||||
isLongPressed = true;
|
||||
update();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function onMouseUp(event: MouseEvent) {
|
||||
clearTimeout(timer);
|
||||
if (isLongPressed) {
|
||||
lastLongPress = event.timeStamp;
|
||||
isLongPressed = false;
|
||||
currentKey = null;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
const foundKey = lookupKey(event);
|
||||
if (foundKey != null) {
|
||||
currentKey = lookupKey(event);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
if (event.timeStamp === lastLongPress) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener('mousedown', onMouseDown);
|
||||
container.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
window.addEventListener('click', onClick, { capture: true });
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('mousedown', onMouseDown);
|
||||
container.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
window.addEventListener('click', onClick, { capture: true });
|
||||
};
|
||||
}, [props.containerRef]);
|
||||
|
||||
return (
|
||||
<FunLightboxKeyContext.Provider value={lightboxKey}>
|
||||
{props.children}
|
||||
</FunLightboxKeyContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal
|
||||
*/
|
||||
|
||||
export type FunLightboxPortalProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunLightboxPortal(props: FunLightboxPortalProps): JSX.Element {
|
||||
return createPortal(props.children, document.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backdrop
|
||||
*/
|
||||
|
||||
export type FunLightboxBackdropProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunLightboxBackdrop(
|
||||
props: FunLightboxBackdropProps
|
||||
): JSX.Element {
|
||||
return <div className="FunLightbox__Backdrop">{props.children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog
|
||||
*/
|
||||
|
||||
export type FunLightboxDialogProps = Readonly<{
|
||||
'aria-label': string;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunLightboxDialog(props: FunLightboxDialogProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
className="FunLightbox__Dialog"
|
||||
aria-label={props['aria-label']}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue