长按确认

按住按钮时,填充色会逐渐铺满;中途松开会退回,按住到底才会确认。

  • JS
  • 按钮
  • 点击
  • 收录于
按住 1.2 秒

AI 提示词

为危险操作制作一个 React 长按确认按钮。在 pointerdown(以及按下空格或回车键)时,启动一个 requestAnimationFrame 循环,在 1200ms 内把进度值从 0 增加到 1,并作为 --p 写到按钮上。用 ::before 填充层设置 transform: scaleX(var(--p)),transform-origin 为左侧。如果在完成前触发 pointerup、pointerleave 或 keyup,就在 250ms 内把 --p 退回 0。进度达到 1 时,文字切换为“已删除”,保持完成状态 1.6 秒后重置。调用 setPointerCapture,让指针移出按钮时依然有效。添加 aria-describedby 指向提示文字“按住 1.2 秒”。在 prefers-reduced-motion 下保持相同的按住时间,但填充分 4 段更新。默认文字为“长按删除”。

源代码

Demo.tsx
'use client'

import { useEffect, useId, useRef, useState } from 'react'
import s from './demo.module.css'

const HOLD = 1200

export default function HoldToConfirm({ labels }: { labels: Record<string, string> }) {
  const ref = useRef<HTMLButtonElement>(null)
  const frame = useRef(0)
  const progress = useRef(0)
  const [done, setDone] = useState(false)
  const hintId = useId()

  useEffect(() => () => cancelAnimationFrame(frame.current), [])

  const set = (p: number) => {
    progress.current = p
    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
    ref.current?.style.setProperty('--p', String(reduced ? Math.floor(p * 4) / 4 : p))
  }

  const start = () => {
    if (done) return
    cancelAnimationFrame(frame.current)
    const from = performance.now() - progress.current * HOLD
    const tick = (now: number) => {
      const p = Math.min((now - from) / HOLD, 1)
      set(p)
      if (p < 1) frame.current = requestAnimationFrame(tick)
      else {
        setDone(true)
        setTimeout(() => {
          setDone(false)
          set(0)
        }, 1600)
      }
    }
    frame.current = requestAnimationFrame(tick)
  }

  const cancel = () => {
    if (done || progress.current >= 1) return
    cancelAnimationFrame(frame.current)
    const from = progress.current
    const t0 = performance.now()
    const tick = (now: number) => {
      const p = from * (1 - Math.min((now - t0) / 250, 1))
      set(p)
      if (p > 0) frame.current = requestAnimationFrame(tick)
    }
    frame.current = requestAnimationFrame(tick)
  }

  const isKey = (e: React.KeyboardEvent) => e.key === ' ' || e.key === 'Enter'

  return (
    <div className={s.wrap}>
      <button
        ref={ref}
        type="button"
        className={s.button}
        data-done={done || undefined}
        aria-describedby={hintId}
        onPointerDown={(e) => {
          e.currentTarget.setPointerCapture(e.pointerId)
          start()
        }}
        onPointerUp={cancel}
        onPointerCancel={cancel}
        onKeyDown={(e) => {
          if (isKey(e) && !e.repeat) {
            e.preventDefault()
            start()
          }
        }}
        onKeyUp={(e) => isKey(e) && cancel()}
      >
        <span className={s.label}>{done ? labels.done : labels.idle}</span>
      </button>
      <span id={hintId} className={s.hint}>
        {labels.hint}
      </span>
    </div>
  )
}
demo.module.css
.wrap { display: grid; justify-items: center; gap: 10px; }

.button {
  --p: 0;
  position: relative;
  overflow: hidden;
  height: 52px;
  padding: 0 28px;
  border: 1px solid var(--stage-rule);
  border-radius: 12px;
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 600 var(--text-base) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
  touch-action: none;
  user-select: none;
}

.button::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--stage-accent);
  transform: scaleX(var(--p));
  transform-origin: left;
}

.label { position: relative; }

.button[data-done] { border-color: var(--stage-accent); color: var(--stage-accent-ink); }
.button[data-done]::before { transform: scaleX(1); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

.hint { font-size: var(--text-xs); color: var(--stage-muted); }

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。