Skip to content

Commit

Permalink
feat(layout): add layout component (#722)
Browse files Browse the repository at this point in the history
Co-authored-by: Dan <liudan548326@yeah.net>
  • Loading branch information
Lee Hon and Dan authored Jan 22, 2021
1 parent 66fa6a1 commit 31e4517
Show file tree
Hide file tree
Showing 13 changed files with 559 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/components/layout/Layout.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Subtitle, ArgsTable } from '@storybook/addon-docs/blocks';
import Layout from './index';

# Layout 布局

<Subtitle>协助进行页面级整体布局。</Subtitle>

## 参数说明

<ArgsTable of={Layout} />

## 代码演示

### 样式 - 无侧导航页面
[Example](/?path=/story/basic-components-layout--default)

### 样式 - 收缩侧导航页面

[Example](/?path=/story/basic-components-layout--sider)

### 样式 - 悬浮式展开侧导航

[Example](/?path=/story/basic-components-layout--suspend)

### 样式 - 嵌入式展开侧导航

[Example](/?path=/story/basic-components-layout--embed)
76 changes: 76 additions & 0 deletions src/components/layout/Layout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import Docs from './Layout.mdx';
import { LayoutProps } from './interfaces';
import Layout from './index';
import Button from '../button';
import './style';

export default {
title: 'Basic Components/Layout',
component: Layout,
parameters: {
docs: {
page: Docs,
},
},
} as Meta;

const Template: Story<LayoutProps> = (args) => <Layout {...args} />;

export const Default = Template.bind({});

Default.args = {
children: (
<>
<Layout.Header style={{ backgroundColor: '#5F87FF' }} />
<Layout.Content style={{ backgroundColor: '#1248E9', height: 400 }} />
</>
),
};

export const Sider = Template.bind({});

Sider.args = {
children: (
<>
<Layout.Sider defaultCollapsed style={{ backgroundColor: '#3867F4' }} />
<Layout>
<Layout.Header style={{ backgroundColor: '#5F87FF' }} />
<Layout.Content style={{ backgroundColor: '#1248E9', height: 400 }} />
</Layout>
</>
),
};

// eslint-disable-next-line react/jsx-props-no-spreading
const DemoListTemplate: Story = (args) => <div {...args} />;

export const Suspend = DemoListTemplate.bind({});

const SuspendDemo = ({ suspend }: { suspend?: 'left' | 'right' }) => {
const [collapsed, setCollapsed] = useState<boolean>(false);
return (
<Layout>
<Layout.Sider collapsed={collapsed} suspendedPosition={suspend} style={{ backgroundColor: '#3867F4' }}>
<Button onClick={() => setCollapsed(!collapsed)} type="secondary">
点我
</Button>
</Layout.Sider>
<Layout>
<Layout.Header style={{ backgroundColor: '#5F87FF' }} />
<Layout.Content style={{ backgroundColor: '#1248E9', height: 400 }} />
</Layout>
</Layout>
);
};

Suspend.args = {
children: <SuspendDemo suspend="left" />,
};

export const Embed = DemoListTemplate.bind({});

Embed.args = {
children: <SuspendDemo />,
};
71 changes: 71 additions & 0 deletions src/components/layout/__tests__/Layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { render } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import Layout from '../layout';
import useSiders from '../useSiders';

describe('Testing Layout', () => {
it('should be stable', () => {
const { asFragment } = render(
<Layout>
<Layout.Sider defaultCollapsed />
<Layout>
<Layout.Header />
<Layout.Content />
</Layout>
</Layout>
);
expect(asFragment()).toMatchSnapshot();
});

test('Content maxWidth', () => {
const { getByText } = render(
<Layout>
<Layout.Content maxWidth="auto">content</Layout.Content>
</Layout>
);
const contentElement = getByText('content');
expect(contentElement.style.getPropertyValue('--layout-content-maxWidth')).toBe('100%');
expect(contentElement.style.getPropertyValue('--layout-content-grow')).toBe('1');
expect(contentElement.style.getPropertyValue('margin')).toBe('0px 20px');
});

test('Content margin', () => {
const { getByText } = render(
<Layout>
<Layout.Content maxWidth="auto" margin={10}>
content
</Layout.Content>
</Layout>
);
expect(getByText('content').style.getPropertyValue('margin')).toBe('0px 10px');
});

test('useSiders', () => {
const { result } = renderHook(() => useSiders());
const _updateSiders = result.current[3];
act(() => {
_updateSiders({ id: '1', width: 200, collapsedWidth: 80, suspendedPosition: 'left' });
_updateSiders({ id: '2', width: 300, collapsedWidth: 100, suspendedPosition: 'right' });
_updateSiders({ id: '3', width: 100, collapsedWidth: 50 });
});
const [siders, sidersWidth, removeSider, updateSiders, margin] = result.current;
expect(siders.length).toBe(3);
expect(sidersWidth).toBe(600);
expect(margin).toStrictEqual([80, 100]);
act(() => {
removeSider('1');
});
expect(result.current[0].length).toBe(2);
expect(result.current[1]).toBe(400);
expect(result.current[4]).toStrictEqual([0, 100]);
act(() => {
updateSiders({ id: '3', width: 100, collapsedWidth: 50, suspendedPosition: 'left' });
});
expect(result.current[4]).toStrictEqual([50, 100]);
act(() => {
removeSider('2');
});
expect(result.current[4]).toStrictEqual([50, 0]);
});
});
27 changes: 27 additions & 0 deletions src/components/layout/__tests__/__snapshots__/Layout.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing Layout should be stable 1`] = `
<DocumentFragment>
<section
class="gio-layout"
style="margin-left: 0px; margin-right: 0px;"
>
<aside
class="gio-layout-sider gio-layout-sider-collapsed"
style="--layout-sider-width: 200px; --layout-sider-collapsedWidth: 80px; --layout-sider-collapsedWidth-negative: -80px;"
/>
<section
class="gio-layout"
style="margin-left: 0px; margin-right: 0px;"
>
<header
class="gio-layout-header"
/>
<main
class="gio-layout-content"
style="--layout-content-maxWidth: 1200px; --layout-content-grow: 0; margin: 0px 20px;"
/>
</section>
</section>
</DocumentFragment>
`;
41 changes: 41 additions & 0 deletions src/components/layout/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useContext, useEffect, useMemo } from 'react';
import classNames from 'classnames';
import { isNumber } from 'lodash';
import { LayoutContentProps } from './interfaces';
import usePrefixCls from '../../utils/hooks/use-prefix-cls';
import { LayoutContext } from './layout';

const Content = ({
prefixCls: customizePrefixCls,
className,
style,
children,
maxWidth = 1200,
margin = 20
}: LayoutContentProps) => {

const { layoutState, setContentState } = useContext(LayoutContext);
useEffect(() => {
setContentState({ maxWidth: isNumber(maxWidth) ? maxWidth : 0, margin });
}, [margin, maxWidth, setContentState]);

const prefixCls = usePrefixCls('layout-content', customizePrefixCls);

const mergedStyle: React.CSSProperties = useMemo(() => ({
'--layout-content-maxWidth': maxWidth === 'auto' ? '100%' : `${maxWidth}px`,
'--layout-content-grow': maxWidth === 'auto' ? 1 : 0,
...((maxWidth === 'auto' || !layoutState.wide) ? { margin: `0 ${margin}px`} : {}),
...style
}), [maxWidth, layoutState.wide, margin, style]);

return (
<main
className={classNames(prefixCls, className)}
style={mergedStyle}
>
{children}
</main>
);
}

export default Content;
19 changes: 19 additions & 0 deletions src/components/layout/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import classNames from 'classnames';
import { LayoutProps } from './interfaces';
import usePrefixCls from '../../utils/hooks/use-prefix-cls';

const Header = ({
prefixCls: customizePrefixCls,
className,
style,
children
}: LayoutProps) => {

const prefixCls = usePrefixCls('layout-header', customizePrefixCls);
return (
<header className={classNames(prefixCls, className)} style={style}>{children}</header>
);
}

export default Header;
4 changes: 4 additions & 0 deletions src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Layout from './layout';

export { LayoutProps } from './interfaces';
export default Layout;
69 changes: 69 additions & 0 deletions src/components/layout/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export interface LayoutProps {
className?: string;
prefixCls?: string;
children?: React.ReactNode | React.ReactNode[];
style?: React.CSSProperties;
}

export interface LayoutContentProps extends LayoutProps {
/**
Content 区域的最大宽度
*/
maxWidth?: number | 'auto';
/*
Content 区域的外边距
*/
margin?: number;
}

export interface LayoutSiderProps extends LayoutProps {
/*
收缩状态的宽度
*/
collapsedWidth?: number;
/*
展开状态的宽度
*/
width?: number;
/*
默认的伸缩状态
*/
defaultCollapsed?: boolean;
/*
控制展开与收缩
*/
collapsed?: boolean;
/*
当伸缩状态变化时的回调函数
*/
onCollapse?: (collapsed: boolean) => void;
/*
悬浮式侧边栏的位置
*/
suspendedPosition?: 'left' | 'right';
}

export interface SiderState {
id: string;
width: number;
collapsedWidth: number;
suspendedPosition?: 'left' | 'right';
}

export interface ContentState {
maxWidth: number;
margin: number;
}

export interface LayoutState {
wide: boolean;
}

export interface LayoutContextType {
layoutState: LayoutState;
contentState: ContentState;
setLayoutState: (layoutState: Partial<LayoutState> | (() => Partial<LayoutState>)) => void;
setContentState: (contentState: Partial<ContentState> | (() => Partial<ContentState>)) => void;
removeSider: (siderId: string) => void;
updateSiders: (sider: SiderState) => void;
}
Loading

1 comment on commit 31e4517

@vercel
Copy link

@vercel vercel bot commented on 31e4517 Jan 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.