126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
'use client';
|
||
|
||
import type { KeyboardEvent } from 'react';
|
||
import { copy } from '@/shared/i18n';
|
||
import { cn } from '@/shared/lib/cn';
|
||
import { useHoldToConfirm } from '../model/useHoldToConfirm';
|
||
|
||
interface ExitHoldButtonProps {
|
||
variant: 'bar' | 'ring';
|
||
onConfirm: () => void;
|
||
className?: string;
|
||
}
|
||
|
||
const RING_RADIUS = 13;
|
||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
|
||
|
||
export const ExitHoldButton = ({
|
||
variant,
|
||
onConfirm,
|
||
className,
|
||
}: ExitHoldButtonProps) => {
|
||
const { progress, isHolding, isCompleted, start, cancel } = useHoldToConfirm(onConfirm);
|
||
const ringOffset = RING_CIRCUMFERENCE * (1 - progress);
|
||
|
||
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||
if (event.key === ' ' || event.key === 'Enter') {
|
||
event.preventDefault();
|
||
start();
|
||
}
|
||
};
|
||
|
||
const handleKeyUp = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||
if (event.key === ' ' || event.key === 'Enter') {
|
||
event.preventDefault();
|
||
cancel();
|
||
}
|
||
};
|
||
|
||
if (variant === 'ring') {
|
||
return (
|
||
<button
|
||
type="button"
|
||
aria-label={copy.space.exitHold.holdToExitAriaLabel}
|
||
onMouseDown={start}
|
||
onMouseUp={cancel}
|
||
onMouseLeave={cancel}
|
||
onTouchStart={start}
|
||
onTouchEnd={cancel}
|
||
onTouchCancel={cancel}
|
||
onKeyDown={handleKeyDown}
|
||
onKeyUp={handleKeyUp}
|
||
onClick={(event) => event.preventDefault()}
|
||
className={cn(
|
||
'relative inline-flex h-9 w-9 items-center justify-center rounded-full bg-white/8 text-white/74 transition hover:bg-white/14 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80',
|
||
isHolding && 'bg-white/16',
|
||
className,
|
||
)}
|
||
>
|
||
<svg
|
||
aria-hidden
|
||
className="-rotate-90 absolute inset-0 h-9 w-9"
|
||
viewBox="0 0 32 32"
|
||
>
|
||
<circle
|
||
cx="16"
|
||
cy="16"
|
||
r={RING_RADIUS}
|
||
fill="none"
|
||
stroke="rgba(255,255,255,0.2)"
|
||
strokeWidth="2"
|
||
/>
|
||
<circle
|
||
cx="16"
|
||
cy="16"
|
||
r={RING_RADIUS}
|
||
fill="none"
|
||
stroke="rgba(186,230,253,0.92)"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeDasharray={RING_CIRCUMFERENCE}
|
||
strokeDashoffset={ringOffset}
|
||
/>
|
||
</svg>
|
||
<span aria-hidden className="relative z-10 text-[12px]">
|
||
⤫
|
||
</span>
|
||
</button>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<button
|
||
type="button"
|
||
aria-label={copy.space.exitHold.holdToExitAriaLabel}
|
||
onMouseDown={start}
|
||
onMouseUp={cancel}
|
||
onMouseLeave={cancel}
|
||
onTouchStart={start}
|
||
onTouchEnd={cancel}
|
||
onTouchCancel={cancel}
|
||
onKeyDown={handleKeyDown}
|
||
onKeyUp={handleKeyUp}
|
||
onClick={(event) => event.preventDefault()}
|
||
className={cn(
|
||
'relative overflow-hidden rounded-lg bg-white/8 px-2.5 py-1.5 text-xs text-white/82 transition hover:bg-white/14 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80',
|
||
className,
|
||
)}
|
||
>
|
||
{isHolding || isCompleted ? (
|
||
<span
|
||
aria-hidden
|
||
className={cn(
|
||
'absolute inset-0 z-0 origin-left transform-gpu bg-sky-200/24 rounded-none',
|
||
isHolding && 'animate-[exit-hold-bar-fill_1000ms_linear_forwards]',
|
||
)}
|
||
style={isCompleted ? { transform: 'scaleX(1)' } : undefined}
|
||
/>
|
||
) : null}
|
||
<span className="relative z-10 inline-flex items-center gap-1">
|
||
<span aria-hidden>⤫</span>
|
||
<span>{copy.space.exitHold.exit}</span>
|
||
</span>
|
||
</button>
|
||
);
|
||
};
|