Design System 101 - Ripple
- 文章發表於
前言
Ripple Effect 是 Material Design 中的一個動畫效果,當使用者點擊 Button 時,會有一個水波紋的效果,讓使用者知道自己點擊的位置。本篇將會介紹如何來實作 Ripple 組件!
API 設計
Ripple 的 API 設計相對簡單,其功能主要會有以下:
- 首先我們需要一個元素作為其附著的範圍,讓 Ripple 組件可以在點擊時呈現水波紋的動畫。
- 讓使用者能夠客製化水波紋的顏色。
屬性 | 描述 | 型別 | 預設值 |
---|---|---|---|
color | Ripple 的顏色 | string | - |
target | Ripple 的附著範圍,Ripple 組件會在這個範圍內呈現動畫 | node | - |
className | Ripple Container 的額外樣式 | string | - |
HTML 結構
我們會透過 <span />
定義其動畫擴張的範圍,由於 Ripple 只是屬於動畫呈現組件,可以用 aria-hidden
來隱藏其元素,增強網頁的無障礙 (Accessibility) 使用性。
<-- container --><span aria-hidden="{true}"><-- animation effect --><span /></span>
使用方式
() => (<div ref={containerRef}><span>Ripple Effect</span><Ripple target={containerRef} /><div>)
建立 Ripple 組件
第一步驟 - 透過 plop 建立 Ripple 的組件
design-system > pnpm generate // name: rippledesign-system > cd packages/rippledesign-system/packages/ripple > pnpm i // 安裝相依套件
第二步驟 - CSS
透過 CSS 來實作 Ripple 的動畫效果,首先先定義 Ripple 的容器,並且透過 overflow: hidden
來限制 Ripple Effect 只會在容器內呈現。
.tocino-Ripple__container {display: block;position: absolute;top: 0;left: 0;z-index: 0;height: 100%;width: 100%;overflow: hidden;pointer-events: none;}
再來就是 Ripple 的動畫效果,當 style 改變時,會透過 transition 來呈現動畫。
.tocino-Ripple {position: absolute;top: 0;left: 0;border-radius: 50%;opacity: 0;pointer-events: none;transform: scale(0.0001, 0.0001);&.tocino--Ripple-animating {transform: none;transition:transform 0.15s linear,width 0.15s linear,height 0.15s linear,opacity 0.15s linear;will-change: transform, width, height, opacity;}&.tocino--Ripple-visible {opacity: 0.3;}}
第三步驟 - 核心邏輯
最後我們就需要監聽使用者點擊或是觸碰的事件,來觸發 Ripple 的動畫效果!
用 useRipple
將主要核心邏輯封裝載該 hook 中:
狀態設計
- rippleStyle: 存放 Ripple 的樣式,並當 Style 改變時,會觸發動畫效果。
- rippleIsVisible: 控制 Ripple 是否可見。
- rippleElRef: Ripple 元素的參考。
export const useRipple = ({ target, color }) => {const [rippleStyle, setRippleStyle] = useState({});const [rippleIsVisible, setRippleIsVisible] = useState(false);const rippleElRef = useRef(null);...}
事件邏輯
接著透過 target
的傳入,我們可以使用 useEffect
來訂閱 tocuh 以及 mouse 事件。
useEffect(() => {target.current?.addEventListener('touchstart', showRipple, { passive: true })target.current?.addEventListener('mousedown', showRipple, { passive: true })target.current?.addEventListener('mouseup', hideRipple, { passive: true })target.current?.addEventListener('mouseleave', hideRipple, { passive: true })return () => {target.current?.removeEventListener('touchstart', showRipple)target.current?.removeEventListener('mousedown', showRipple)target.current?.removeEventListener('mouseup', hideRipple)target.current?.removeEventListener('mouseleave', hideRipple)}}, [])
當使者點擊容器時,會觸發 showRipple
事件,並且透過 rippleElRef
來取得 ripple 的元素,並且計算出 ripple 的位置。
const showRipple = useCallback((evt) => {const buttonEl = target.currentconst offset = domUtils.offset(buttonEl)const clickEvent = evt.type === 'touchstart' && evt.touches ? evt.touches[0] : evtconst radius = Math.sqrt(offset.width * offset.width + offset.height * offset.height)const diameterPx = radius * 2 + 'px'setRippleStyle({top: Math.round(clickEvent.pageY - offset.top - radius) + 'px',left: Math.round(clickEvent.pageX - offset.left - radius) + 'px',width: diameterPx,height: diameterPx,backgroundColor: color,})setRippleIsVisible(true)},[rippleElRef, color])
最後在事件結束後,觸發 hideRipple
事件,讓 Ripple Effect 消失。
const hideRipple = useCallback(() => {setRippleIsVisible(false)}, [])
import React from 'react' import { Ripple } from './ripple' export default () => { const containerRef = React.useRef(null) return ( <div ref={containerRef} style={{ position: 'relative', width: '200px', height: '200px', border: '1px solid gray', display: 'flex', justifyContent: 'center', alignItems: 'center', borderRadius: '50%', }} > <span>Ripple Effect</span> <Ripple target={containerRef} /> </div> ) }
開啟 Storybook & 測試
design-system/ pnpm run test -wdesign-system/ pnpm run storybook
透過 changeset 來產生 changelog 以及 commit
pnpm changeset