# 用 React 實作 Tabs 元件 Elantris [TOC] --- ## 元件分析 ### 組成結構 - 按鈕列 (Bar) - 標籤按鈕 (Item) - 內容 (Content) - 內容框 (Pane) ```html <div class="tabs"> <div class="tabs-bar"> <button class="tabs-item active"></button> <button class="tabs-item"></button> <button class="tabs-item"></button> </div> <div class="tabs-content"> <div class="tabs-pane active"></div> <div class="tabs-pane"></div> <div class="tabs-pane"></div> </div> </div> ``` ### 互動邏輯 1. 只顯示其中一個內容框 2. 點擊按鈕列上的標籤切換要顯示的內容框 ## React 元件設計 ### 最小可行性元件 最基本的資料所需的參數 - `key` 用來對應每一筆資料 - `label` 顯示於按鈕列上的標籤 - `content` 顯示內容 ```typescript type ItemProps = { key: string label: string content: React.ReactNode } ``` 理論上元件 props 只需要 `Item` 陣列即可,`active` 狀態由內部 state 控制: ```tsx const Tabs: React.FC<{ items: ItemProps[] }> = ({ items }) => { const [activeKey, setActiveKey] = React.useState(items[0].key) return ( <div className="tabs"> <div className="tabs-bar"> {items.map((item) => { const isActive = item.key === activeKey return ( <button key={item.key} className={`tabs-item ${isActive ? 'active' : ''}`} onClick={() => setActiveKey(item.key)} > {item.label} </button> ) })} </div> <div className="tabs-content"> {items.map((item) => { const isActive = item.key === activeKey return ( <div key={item.key} className={`tabs-pane ${isActive ? 'active' : ''}`}> {item.content} </div> ) })} </div> </div> ) } ``` ### 增加外部控制參數 為了讓這個元件更方便使用加上一些 props,渲染時優先取用外部狀態: ```tsx Tabs: React.FC<{ items: ItemProps[] defaultActiveKey?: string activeKey?: string onChange?: (key: string) => void }> = ({ items, defaultActiveKey, activeKey, onChange }) => { const [_activeKey, _setActiveKey] = React.useState(defaultActiveKey ?? items[0].key) const __activeKey = activeKey ?? _activeKey // ... } ``` - `defaultActiveKey` 由外部指定預設要顯示的內容,只影響內部狀態的初始值 - `activeKey` 由外部直接指定要顯示的內容,內部狀態加上底線前綴 - `onChange` 點擊標籤按鈕時同步呼叫外部 onChange 將目標資料帶到外部去處理 如此一來在取用這個元件時可以更自由且精準地控制每一個狀態,例如綁定 router 路徑參數或根據情境實現特定操作邏輯。 ## CSS 樣式 在切換內容時因為想要有進場動畫的特效,比較合理的作法是透過 class 改變 CSS 屬性、觸發 transition 動畫。 ```css .tabs-content { position: relative; overflow: hidden; } .tabs-pane { position: absolute; top: 100%; width: 100%; transform: translateY(-8px); opacity: 0; &.active { position: unset; top: unset; transition: all 0.5s ease-in-out; transform: translateY(0); opacity: 1; } } ``` 1. 預設隱藏狀態 - 透過絕對定位將整個 `tabs-pane` 移出顯示區域,並且隱藏在外層 overflow hidden 中 - `tabs-pane` 固定寬度 100% 與 `tabs-content` 相同,維持 box model 尺寸 2. 從隱藏狀態切換為顯示狀態 - 加上 transition 屬性觸發進場動畫 - positoin、top 的數值在兩個狀態之間不是線性變化,不屬於 transition 作用的範圍而不受影響 - transform 與 opacity 製造出由上而下淡入的進場動畫 3. 從顯示狀態切換為隱藏狀態 - 因為失去了 transition 屬性導致所有屬性變化都是瞬間切換完成,畫面上就會看起來是瞬間消失,並且銜接另一個 `tabs-pane` 的進場動畫 - 可以透過 transition-delay 實現進場、離場動畫同時存在的效果