本系列文章會在筆者的部落格繼續連載!Design System 101 感謝大家的閱讀!
Layout,它就像是一個家的格局,通常裝潢房子前的第一件事就是先規劃格局,規劃好格局後,就能夠更清楚知道如何運用空間,例如傢俱的擺設、電器的放置等等。而好的格局可以讓整體空間看起來更舒適。
同樣的道理也適用在網頁中,Layout 扮演了整個頁面的骨架,將其架好之後,就可以將想傳達給使用者的訊息組件放入。當這個骨架重複應用在各個頁面中,就可以讓使用者擁有一致的使用體驗,而這也是 Design System 的基礎。
在網頁中最常見的排列方式為網格系統,其為排列的工具。它定義了怎麼放組件、以及其間距和大小。這一套規則不只應用在網格,還包括字型和 Icon 的設定。組件應該要遵照這個規則,讓整個頁面看起來更一致。
今天將來介紹兩種 Layout 的排列方式,分別是 Grid 和 Flex。
大家對於 CSS 的 flexbox 應該不陌生,通常是用來處理單維度的排列,例如水平或垂直。
屬性 | 說明 | 值 |
---|---|---|
align |
對齊方式 | start , center , end ... |
gap |
元素間距 | number |
wrap |
換行方式 | nowrap , wrap .. |
direction |
排列方向 | row , row-reverse , column ... |
justify |
對齊方式 | start , center , end ... |
接著就開始來實作 Flex 組件,而這也是相對簡單的組件,想要詳細知道 flexbox 是如何使用,可以參考 flexbox。
首先先建立 FlexEl
組件,並將 props
傳入 useFlexProps
中。
import React from 'react';
import { useFlexProps } from './useBreakpoint';
const FlexEl = (props, ref) => {
const flexProps = useFlexProps(props);
return (
<div {...flexProps} ref={ref}>
{props.children}
</div>
);
};
export const Flex = React.forwardRef(FlexEl);
建立useFlexProps
會根據 props
產生對應的 style
,並且會根據 breakpoint
來決定要使用哪個值。
// useBreakpoint
import { useState, useEffect } from 'react';
const useBreakpoint = () => {
const getBreakpoint = () => {
const width = window.innerWidth;
if (width < 600) return 'xxs';
if (width < 905) return 'xs';
if (width < 1240) return 's';
if (width < 1440) return 'm';
return 'l';
};
const [breakpoint, setBreakpoint] = useState(getBreakpoint);
useEffect(() => {
const onResize = () => {
setBreakpoint(getBreakpoint());
};
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return breakpoint;
};
export const useFlexProps = (props) => {
const { align, gap, wrap, direction, justify } = props;
const breakpoint = useBreakpoint();
const resolveResponsiveValue = (value) => {
if (Array.isArray(value)) {
const index = ['xxs', 'xs', 's', 'm', 'l'].indexOf(breakpoint);
return value[Math.min(index, value.length - 1)];
}
return value;
};
return {
style: {
display: 'flex',
'align-items': resolveResponsiveValue(align),
'flex-wrap': resolveResponsiveValue(wrap),
'flex-direction': resolveResponsiveValue(direction),
'justify-content': resolveResponsiveValue(justify),
gap: resolveResponsiveValue(gap),
},
};
};
而以下是 Flex 組件的使用範例
import { Flex } from './flex'
export default () => {
return (
<Flex direction={['column', 'row']} gap={['10px', '40px']}>
<div style={{ width: '100px', height: '100px', backgroundColor: '#4F378B' }} />
<div style={{ width: '100px', height: '100px', backgroundColor: '#4A4458' }} />
</Flex>
);
};
Grid 則是用在二維度的排列,使用 Grid 可以讓我們輕易地將網頁的版面切割成多個區塊。想要詳細知道 flexbox 是如何使用,可以參考 grid。
Grid
屬性 | 說明 | 值 |
---|---|---|
areas | 定義網格區域 | string |
cols | 義網格列的數量、寬度和輸入順序 | number |
gap | 定義網格元素之間的間距,包括行間距和列間距 | string |
rows | 定義網格行的數量、高度和輸入順序 | number |
GridItem
屬性 | 說明 | 值 |
---|---|---|
area | 定義網格區域 | string |
col | 定義元素橫跨的網格列的起始和結束位置 | number |
row | 定義元素橫跨的網格行的起始和結束位置 | number |
在實作概念上跟 Grid 跟 Flex 組件差不多
但為了讓 useFlexProps
能夠重複使用,所以將 useFlexProps
改成 useLayoutProps
,並且透過 useLayoutProps
傳入的 name 來決定要使用哪種排列方式。
// useLayoutProps
import React from 'react';
import { useBreakpoint } from './useBreakpoint';
function getFormattedAreas(areas) {
return `'${areas.toString().replace(/,/g, "' '")}'`;
}
const CSS_LAYOUT = {
grid: [
{ areas: 'grid-template-areas' },
{ cols: 'grid-template-columns' },
{ rows: 'grid-template-rows' },
{ gap: 'grid-column-gap' },
],
flex: [
{ align: 'align-items' },
{ direction: 'flex-direction' },
{ justify: 'justify-content' },
{ gap: 'gap' },
{ wrap: 'flex-wrap' },
],
gridItem: [{ area: 'grid-area' }, { col: 'grid-column' }, { row: 'grid-row' }],
};
const DEFAULT_CSS = {
grid: {
display: 'grid',
},
flex: {
display: 'flex',
},
};
const useLayoutProps = (props = {}, name) => {
const breakpoint = useBreakpoint();
const resolveResponsiveValue = (value) => {
if (Array.isArray(value)) {
const index = ['xxs', 'xs', 's', 'm', 'l'].indexOf(breakpoint);
return value[Math.min(index, value.length - 1)];
}
return value;
};
const layoutProps = CSS_LAYOUT[name];
const layoutStyle = layoutProps.reduce(
(acc, prop) => {
const [key, cssProp] = Object.entries(prop)[0];
const value = name === 'grid' ? getFormattedAreas(props[key]) : props[key];
if (value) {
acc[cssProp] = resolveResponsiveValue(value);
}
return acc;
},
{ ...(DEFAULT_CSS[name] || {}) },
);
return { style: { ...(props.style || {}), ...layoutStyle } };
};
再來建立 Grid 與 GridItem 組件
import { useLayoutProps } from './useLayoutProps'
const GridItemEl = (props, ref) => {
const gridItemProps = useLayoutProps(props, 'gridItem');
return (
<div {...gridItemProps} ref={ref}>
{props.children}
</div>
);
};
const GridEl = (props, ref) => {
const gridProps = useLayoutProps(props, 'grid');
return (
<div {...gridProps} ref={ref}>
{props.children}
</div>
);
};
export const Grid = React.forwardRef(GridEl);
export const GridItem = React.forwardRef(GridItemEl);
最後就可以透過該組件,建立 Layout
import { Grid, GridItem } from './grid'
export default () => {
return (
<Grid areas={['header header', 'nav main', 'footer footer']} cols="12" gap="6" rows="2rem 10rem 5rem">
<GridItem area="header" col="1 / span 12" style={{ backgroundColor: 'green' }}>
Header
</GridItem>
<GridItem area="nav" col="1 / span 3" style={{ backgroundColor: 'aliceblue' }}>
Navigation
</GridItem>
<GridItem area="main" col="4 / span 9" style={{ backgroundColor: 'yellow' }}>
Main
</GridItem>
<GridItem area="footer" col="1 / span 12" style={{ backgroundColor: 'red' }}>
Footer
</GridItem>
</Grid>
);
};
明天將會介紹 Tab 組件!