LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

Organization

Ripple Button

本日は、押下された位置に RippleEffect を発生させるボタンの実装を解説します。
code: github / $ yarn 1203

1203.jpg 1203-1.jpg

コンポーネントの使用感は以下の通りです。

<RippleButton
  width={'160px'}
  heihgt={'64px'}
  borderRadius={'5px'}
  backgroundColor={'#7089b9'}
  effectSize={160}
>
  <span className="children">
    <SVGInline svg={UP1} />
    UPLOAD
  </span>
</RippleButton>

見ての通りで、style設定を props で受けつけます。また、ボタン内部は自由にコンテンツが配備出来る様、props.children を render します。

Custom Hooks の切り出し

View とロジックの住み分けのため、useRippleEffect という Custom Hooks を定義します。戻り値には、コンポーネントで必要になる算出プロパティと更新ハンドラを含めます(ここでは、適用する style、マウスダウンハンドラ、マウスアップハンドラ)。各 Hooks API にバインドしている処理詳細は、解説上割愛します。

function useRippleEffect(props: Props) {
  const [state, update] = useState<State>(...)
  const tx = useMemo(...)
  const ty = useMemo(...)
  const ts = useMemo(...)
  const effectSytle = useMemo(...)
  const handleMouseDown = useCallback(...)
  const handleMouseUp = useCallback(...)
  return {
    effectSytle,
    handleMouseDown,
    handleMouseUp
  }
}

Ref Injection

従来 ref は Statefull Component でのみ利用出来ましたが、useRef を使うことで FC でも ref が利用可能になりました。ref は、Custom Hooks に含めず「useRippleEffect」を利用する Component 上で定義し、注入します。これは、ref が複数 Custom Hooks の興味対象となる可能性があるためです。

const View = (props: Props) => {
  const ref = useRef({} as HTMLButtonElement)
  const {
    handleMouseDown,
    handleMouseUp,
    effectSytle
  } = useRippleEffect({
    ref,
    effectDuration: props.effectDuration
  })
  return (
    <button
      ref={ref}
      className={props.className}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onClick={props.onClick}
    >
      <span className="effect" style={effectSytle} />
      {props.children}
    </button>
  )
}

位置の算出

波紋は DOM を用いて表現します。ボタン押下時に、要素の相対位置とボタン座標から、波紋DOM の中央位置を特定します。この時、アニメーション動力になる「transitionDuration」を 0 に指定します。

const handleMouseDown = useCallback(
    (event: MouseEvent<HTMLButtonElement>) => {
      event.persist()
      update(_state => {
        if (props.ref.current === null) return _state
        const clickX = event.pageX
        const clickY = event.pageY
        const clientRect = props.ref.current.getBoundingClientRect()
        const positionX = clientRect.left + window.pageXOffset
        const positionY = clientRect.top + window.pageYOffset
        const transformX = clickX - positionX
        const transformY = clickY - positionY
        return {
          ..._state,
          opacity: 0.5,
          transformX,
          transformY,
          transformScale: 0,
          transitionDuration: 0
        }
      })
    },
    []
  )

今回のサンプル集では、useState で扱う状態は積極的に object にまとめています。先日の投稿による意図も勿論ありますが、この様にまとめて状態を更新する必要がある場合、state毎に useState するよりも便利です。

CSSアニメーション発火

マウスアップ時、動かしたいプロパティと供に、transitionDuration を付与するとアニメーションが発火します。

const handleMouseUp = useCallback(
  (event: MouseEvent<HTMLButtonElement>) => {
    event.persist()
    update(_state => ({
      ..._state,
      opacity: 0,
      transformScale: 1,
      transitionDuration: _state.effectDuration
    }))
  },
  []
)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1