今天就要來實作 Icon 啦!事不宜遲直接開始!
想先看 Code 或是 Demo 的由此去:
Github Repo: ithelp-ui-demo
Live Demo:Storybook
在 Icon 這邊首先要建立一個觀念,就是 「網頁中的 Icon 基本上都會是以 SVG 的格式來呈現」。
使用 SVG 的原因主要是用來減少圖片的使用率,透過向量圖形把 Icon 畫出來並當成文字來調整大小 (font-size)、和顏色 (color)。
更詳細的理由可以看 MDN - 為何使用 SVG ?
而在這邊能使用 font-size 來改變大小的原因是因為我在寬高是使用 1em 來設定,em 的意思是以父元素的倍數來呈現,因此在父元素指定 font-size 就能改變 Icon 大小啦,而沒給的話就是吃到最頂層預設的值。
參考 一次搞懂 CSS 字體單位:px、em、rem 和 %
那既然都是用 SVG 來 Render 的話,其實就可以把 SVG 會用到的屬性統一出一個規格,再透過框架各自的語法把它 Render 出來。
順著這個邏輯,以下會來逐步介紹 IconDefinition、Icon interface 和 Icon Component,帶大家一探 Icon 是如何被元件化的!
首先來看看能如何定義一個 IconDefinition 的介面 ,讓日後新引入的 Icon 都能遵循這個格式來繪製。
export interface IconDefinition {
name: string;
definition: {
svg?: {
viewBox?: string; // default: 0 0 24 24
};
path?: {
d?: string;
fill?: string;
fillRule?: 'nonzero' | 'evenodd' | 'inherit';
stroke?: string;
strokeWidth?: string | number;
transform?: string;
};
};
}
接著直接來看看一個 Icon 能怎麼透過 SVG 畫出來,以 check 為例:
import { IconDefinition } from "./typings";
export const CheckIcon: IconDefinition = {
name: "check",
definition: {
svg: {
viewBox: "0 0 24 24",
},
path: {
fill: "currentColor",
fillRule: "evenodd",
stroke: "none",
strokeWidth: 1,
d:
"M17.993 8.768l-7.625 7.625L6.4 12.425l1.061-1.061 2.908 2.907 6.564-6.563z",
},
},
};
以此就可以知道其實要用 SVG 畫出一個 Icon 的話只需要像上面這樣定義,主要就是 viewBox 確認比例,然後設定一下 fill、fillRule、stroke、strokeWidth,接著運用 d 這個屬性把線都畫出來。
那我之前的經驗是可以請 UI 設計師遵從 viewBox: "0 0 24 24" 這樣的比例把 SVG 切出來給我,如此統一規格之後,未來就可以無痛新增 Icon 了。
想看看其他 Icon 的 Definition 請參照 Github。
這邊在簡短總結一下 IconDefinition :「藉由制定一套 Icon 的 SVG 介面,日後想新增客製化的 Icon 只要照著這介面去繪製即可。」
既然統一了 Icon 的 SVG 規格之後,我們就可以接著來看看 Icon 這個元件的 Interface 和在 React 上如何實作它了!
最大的重點在 IconDefinition 都說完了,剩餘的兩個屬性挺白話的,就直接看 Storybook 的 Description 吧!
最後在實作這邊就只是在用 React JSX 的語法把 IconDefinition 丟下去 Render 出對應的 Icon 而已。
export const Icon: React.FC<IconProps> = (props) => {
const { className, color = "black", icon, spin = false } = props;
const { definition } = icon;
return (
<i
aria-hidden
className={`
inline-block flex-shrink-0 select-none w-em h-em
${spin ? "animate-spin" : ""}
${color ? Color[color] : ""}
${className ? className : ""}
`}
data-icon-name={icon.name}
>
<svg {...definition.svg} focusable={false}>
<path {...definition.path} />
</svg>
</i>
);
};
上面講完大家可能還是沒感受到 IconDefinition 的力量,因此這邊直接運用同一套 Definition 來用 Angular 的 Template 語法上去呈現。
template: `
<svg
[attr.focusable]="false"
[attr.viewBox]="svg?.viewBox"
>
<svg:path
*ngIf="path"
[attr.d]="path.d"
[attr.fill]="path.fill"
[attr.fill-rule]="path.fillRule"
[attr.stroke]="path.stroke"
[attr.stroke-width]="path.strokeWidth"
[attr.transform]="path.transform"
>
</svg:path>
</svg>
`,
get svg() {
return this.icon.definition.svg;
}
get path() {
return this.icon.definition.path;
}
由此可知,有時候介面定義出來,不同框架就只是在用不同的語法去實作而已。
相信介紹完 Icon 之後,大家會對介面的定義能更有感觸,我第一次知道 Icon 能用這樣的方式來產出來的時候,也是驚為天人呢,沒想到可以幫一個個 Icon 抽象化成這樣的 SVG 格式!
明天要介紹的事在 Design System 的 System 實作上的最後一個元素 — 元件動畫(Motion)的過渡(Transition)。
如果覺得對你有幫助的話,希望大家可以不吝在 Github Repo 上給個 Star ><
那就明天見囉!