refactor: layout

pull/4134/head
tanjinzhou 2021-05-26 14:47:50 +08:00
parent fe99051b55
commit 3825c6507f
9 changed files with 231 additions and 235 deletions

View File

@ -1,17 +1,25 @@
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { inject, provide, PropType, defineComponent, nextTick } from 'vue'; import {
inject,
PropType,
defineComponent,
ExtractPropTypes,
ref,
watch,
onMounted,
onBeforeUnmount,
CSSProperties,
provide,
} from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type'; import { tuple } from '../_util/type';
import { getOptionProps, hasProp, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps'; import initDefaultProps from '../_util/props-util/initDefaultProps';
import BaseMixin from '../_util/BaseMixin';
import isNumeric from '../_util/isNumeric'; import isNumeric from '../_util/isNumeric';
import { defaultConfigProvider } from '../config-provider';
import BarsOutlined from '@ant-design/icons-vue/BarsOutlined'; import BarsOutlined from '@ant-design/icons-vue/BarsOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined'; import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
import omit from 'omit.js'; import useConfigInject from '../_util/hooks/useConfigInject';
import { SiderHookProvider } from './layout'; import { SiderCollapsedKey, SiderHookProviderKey } from './injectionKey';
const dimensionMaxMap = { const dimensionMaxMap = {
xs: '479.98px', xs: '479.98px',
@ -24,7 +32,7 @@ const dimensionMaxMap = {
export type CollapseType = 'clickTrigger' | 'responsive'; export type CollapseType = 'clickTrigger' | 'responsive';
export const SiderProps = { export const siderProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
collapsible: PropTypes.looseBool, collapsible: PropTypes.looseBool,
collapsed: PropTypes.looseBool, collapsed: PropTypes.looseBool,
@ -40,6 +48,7 @@ export const SiderProps = {
onCollapse: Function as PropType<(collapsed: boolean, type: CollapseType) => void>, onCollapse: Function as PropType<(collapsed: boolean, type: CollapseType) => void>,
}; };
export type SiderProps = Partial<ExtractPropTypes<typeof siderProps>>;
// export interface SiderState { // export interface SiderState {
// collapsed?: boolean; // collapsed?: boolean;
// below: boolean; // below: boolean;
@ -61,10 +70,8 @@ const generateId = (() => {
export default defineComponent({ export default defineComponent({
name: 'ALayoutSider', name: 'ALayoutSider',
mixins: [BaseMixin],
inheritAttrs: false, inheritAttrs: false,
__ANT_LAYOUT_SIDER: true, props: initDefaultProps(siderProps, {
props: initDefaultProps(SiderProps, {
collapsible: false, collapsible: false,
defaultCollapsed: false, defaultCollapsed: false,
reverseArrow: false, reverseArrow: false,
@ -72,173 +79,141 @@ export default defineComponent({
collapsedWidth: 80, collapsedWidth: 80,
}), }),
emits: ['breakpoint', 'update:collapsed', 'collapse'], emits: ['breakpoint', 'update:collapsed', 'collapse'],
setup() { setup(props, { emit, attrs, slots }) {
return { const { prefixCls } = useConfigInject('layout-sider', props);
siderHook: inject<SiderHookProvider>('siderHook', {}), const siderHook = inject(SiderHookProviderKey);
configProvider: inject('configProvider', defaultConfigProvider), const collapsed = ref(
}; !!(props.collapsed !== undefined ? props.collapsed : props.defaultCollapsed),
},
data() {
const uniqueId = generateId('ant-sider-');
let matchMedia: typeof window.matchMedia;
if (typeof window !== 'undefined') {
matchMedia = window.matchMedia;
}
const props = getOptionProps(this) as any;
let mql: MediaQueryList;
if (matchMedia && props.breakpoint && props.breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
}
let sCollapsed: boolean;
if ('collapsed' in props) {
sCollapsed = props.collapsed;
} else {
sCollapsed = props.defaultCollapsed;
}
return {
sCollapsed,
below: false,
belowShow: false,
uniqueId,
mql,
};
},
watch: {
collapsed(val) {
this.setState({
sCollapsed: val,
});
},
},
created() {
provide('layoutSiderContext', this); // menu使
},
mounted() {
nextTick(() => {
if (this.mql) {
this.mql.addListener(this.responsiveHandler);
this.responsiveHandler(this.mql);
}
if (this.siderHook.addSider) {
this.siderHook.addSider(this.uniqueId);
}
});
},
beforeUnmount() {
if (this.mql) {
this.mql.removeListener(this.responsiveHandler);
}
if (this.siderHook.removeSider) {
this.siderHook.removeSider(this.uniqueId);
}
},
methods: {
responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) {
this.setState({ below: mql.matches });
this.$emit('breakpoint', mql.matches);
if (this.sCollapsed !== mql.matches) {
this.setCollapsed(mql.matches, 'responsive');
}
},
setCollapsed(collapsed: boolean, type: CollapseType) {
if (!hasProp(this, 'collapsed')) {
this.setState({
sCollapsed: collapsed,
});
}
this.$emit('update:collapsed', collapsed);
this.$emit('collapse', collapsed, type);
},
toggle() {
const collapsed = !this.sCollapsed;
this.setCollapsed(collapsed, 'clickTrigger');
},
belowShowChange() {
this.setState({ belowShow: !this.belowShow });
},
},
render() {
const {
prefixCls: customizePrefixCls,
class: className,
theme,
collapsible,
reverseArrow,
style,
width,
collapsedWidth,
zeroWidthTriggerStyle,
...others
} = { ...getOptionProps(this), ...this.$attrs } as any;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls);
const divProps = omit(others, [
'collapsed',
'defaultCollapsed',
'onCollapse',
'breakpoint',
'onBreakpoint',
'siderHook',
'zeroWidthTriggerStyle',
'trigger',
]);
const trigger = getComponent(this, 'trigger');
const rawWidth = this.sCollapsed ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
// special trigger when collapsedWidth == 0
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={this.toggle}
class={`${prefixCls}-zero-width-trigger ${prefixCls}-zero-width-trigger-${
reverseArrow ? 'right' : 'left'
}`}
style={zeroWidthTriggerStyle}
>
<BarsOutlined />
</span>
) : null;
const iconObj = {
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />,
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />,
};
const status = this.sCollapsed ? 'collapsed' : 'expanded';
const defaultTrigger = iconObj[status];
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div class={`${prefixCls}-trigger`} onClick={this.toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
: null;
const divStyle = {
...style,
flex: `0 0 ${siderWidth}`,
maxWidth: siderWidth, // Fix width transition bug in IE11
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
width: siderWidth,
};
const siderCls = classNames(className, prefixCls, `${prefixCls}-${theme}`, {
[`${prefixCls}-collapsed`]: !!this.sCollapsed,
[`${prefixCls}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger,
[`${prefixCls}-below`]: !!this.below,
[`${prefixCls}-zero-width`]: parseFloat(siderWidth) === 0,
});
return (
<aside class={siderCls} {...divProps} style={divStyle}>
<div class={`${prefixCls}-children`}>{getSlot(this)}</div>
{collapsible || (this.below && zeroWidthTrigger) ? triggerDom : null}
</aside>
); );
const below = ref(false);
watch(
() => props.collapsed,
() => {
collapsed.value = !!props.collapsed;
},
);
provide(SiderCollapsedKey, collapsed);
const handleSetCollapsed = (value: boolean, type: CollapseType) => {
if (props.collapsed === undefined) {
collapsed.value = value;
}
emit('update:collapsed', value);
emit('collapse', value, type);
};
// ========================= Responsive =========================
const responsiveHandlerRef = ref<(mql: MediaQueryListEvent | MediaQueryList) => void>(
(mql: MediaQueryListEvent | MediaQueryList) => {
below.value = mql.matches;
emit('breakpoint', mql.matches);
if (collapsed.value !== mql.matches) {
handleSetCollapsed(mql.matches, 'responsive');
}
},
);
let mql: MediaQueryList;
function responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) {
return responsiveHandlerRef.value!(mql);
}
const uniqueId = generateId('ant-sider-');
onMounted(() => {
if (typeof window !== 'undefined') {
const { matchMedia } = window;
if (matchMedia! && props.breakpoint && props.breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
try {
mql.addEventListener('change', responsiveHandler);
} catch (error) {
mql.addListener(responsiveHandler);
}
responsiveHandler(mql);
}
}
siderHook && siderHook.addSider(uniqueId);
});
onBeforeUnmount(() => {
try {
mql?.removeEventListener('change', responsiveHandler);
} catch (error) {
mql?.removeListener(responsiveHandler);
}
siderHook && siderHook.removeSider(uniqueId);
});
const toggle = () => {
handleSetCollapsed(!collapsed.value, 'clickTrigger');
};
return () => {
const pre = prefixCls.value;
const {
collapsedWidth,
width,
reverseArrow,
zeroWidthTriggerStyle,
trigger,
collapsible,
theme,
} = props;
const rawWidth = collapsed.value ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
// special trigger when collapsedWidth == 0
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={toggle}
class={classNames(
`${pre}-zero-width-trigger`,
`${pre}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`,
)}
style={zeroWidthTriggerStyle}
>
{trigger || <BarsOutlined />}
</span>
) : null;
const iconObj = {
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />,
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />,
};
const status = collapsed.value ? 'collapsed' : 'expanded';
const defaultTrigger = iconObj[status];
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div class={`${pre}-trigger`} onClick={toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
: null;
const divStyle = {
...(attrs.style as CSSProperties),
flex: `0 0 ${siderWidth}`,
maxWidth: siderWidth, // Fix width transition bug in IE11
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
width: siderWidth,
};
const siderCls = classNames(
pre,
`${pre}-${theme}`,
{
[`${pre}-collapsed`]: !!collapsed.value,
[`${pre}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger,
[`${pre}-below`]: !!below.value,
[`${pre}-zero-width`]: parseFloat(siderWidth) === 0,
},
attrs.class,
);
return (
<aside {...attrs} class={siderCls} style={divStyle} ref={ref}>
<div class={`${pre}-children`}>{slots.default?.()}</div>
{collapsible || (below && zeroWidthTrigger) ? triggerDom : null}
</aside>
);
};
}, },
}); });

View File

@ -2,6 +2,9 @@ import { App, Plugin } from 'vue';
import Layout from './layout'; import Layout from './layout';
import Sider from './Sider'; import Sider from './Sider';
export { BasicProps as LayoutProps } from './layout';
export { SiderProps } from './Sider';
Layout.Sider = Sider; Layout.Sider = Sider;
/* istanbul ignore next */ /* istanbul ignore next */

View File

@ -0,0 +1,12 @@
import { Ref, InjectionKey } from 'vue';
export type SiderCollapsed = Ref<boolean>;
export const SiderCollapsedKey: InjectionKey<SiderCollapsed> = Symbol('siderCollapsed');
export interface SiderHookProvider {
addSider?: (id: string) => void;
removeSider?: (id: string) => void;
}
export const SiderHookProviderKey: InjectionKey<SiderHookProvider> = Symbol('siderHookProvider');

View File

@ -1,17 +1,8 @@
import { import { createVNode, defineComponent, provide, ref, ExtractPropTypes, HTMLAttributes } from 'vue';
createVNode,
defineComponent,
inject,
provide,
toRefs,
ref,
ExtractPropTypes,
HTMLAttributes,
} from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { defaultConfigProvider } from '../config-provider'; import useConfigInject from '../_util/hooks/useConfigInject';
import { flattenChildren } from '../_util/props-util'; import { SiderHookProviderKey } from './injectionKey';
export const basicProps = { export const basicProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
@ -21,40 +12,29 @@ export const basicProps = {
export type BasicProps = Partial<ExtractPropTypes<typeof basicProps>> & HTMLAttributes; export type BasicProps = Partial<ExtractPropTypes<typeof basicProps>> & HTMLAttributes;
export interface SiderHookProvider {
addSider?: (id: string) => void;
removeSider?: (id: string) => void;
}
type GeneratorArgument = { type GeneratorArgument = {
suffixCls: string; suffixCls: string;
tagName: string; tagName: 'header' | 'footer' | 'main' | 'section';
name: string; name: string;
}; };
function generator({ suffixCls, tagName, name }: GeneratorArgument) { function generator({ suffixCls, tagName, name }: GeneratorArgument) {
return (BasicComponent: typeof Basic) => { return (BasicComponent: typeof Basic) => {
const Adapter = defineComponent<BasicProps>({ const Adapter = defineComponent({
name, name,
props: basicProps,
setup(props, { slots }) { setup(props, { slots }) {
const { getPrefixCls } = inject('configProvider', defaultConfigProvider); const { prefixCls } = useConfigInject(suffixCls, props);
return () => { return () => {
const { prefixCls: customizePrefixCls } = props;
const prefixCls = getPrefixCls(suffixCls, customizePrefixCls);
const basicComponentProps = { const basicComponentProps = {
prefixCls, prefixCls: prefixCls.value,
...props,
tagName, tagName,
...props,
}; };
return ( return <BasicComponent {...basicComponentProps}>{slots.default?.()}</BasicComponent>;
<BasicComponent {...basicComponentProps}>
{flattenChildren(slots.default?.())}
</BasicComponent>
);
}; };
}, },
}); });
Adapter.props = basicProps;
return Adapter; return Adapter;
}; };
} }
@ -62,30 +42,32 @@ function generator({ suffixCls, tagName, name }: GeneratorArgument) {
const Basic = defineComponent({ const Basic = defineComponent({
props: basicProps, props: basicProps,
setup(props, { slots }) { setup(props, { slots }) {
const { prefixCls, tagName } = toRefs(props); return () => createVNode(props.tagName, { class: props.prefixCls }, slots.default?.());
return () => createVNode(tagName.value, { class: prefixCls.value }, slots.default?.());
}, },
}); });
const BasicLayout = defineComponent({ const BasicLayout = defineComponent({
props: basicProps, props: basicProps,
setup(props, { slots }) { setup(props, { slots }) {
const { direction } = useConfigInject('', props);
const siders = ref<string[]>([]); const siders = ref<string[]>([]);
const siderHookProvider: SiderHookProvider = { const siderHookProvider = {
addSider: id => { addSider: (id: string) => {
siders.value = [...siders.value, id]; siders.value = [...siders.value, id];
}, },
removeSider: id => { removeSider: (id: string) => {
siders.value = siders.value.filter(currentId => currentId !== id); siders.value = siders.value.filter(currentId => currentId !== id);
}, },
}; };
provide('siderHook', siderHookProvider);
provide(SiderHookProviderKey, siderHookProvider);
return () => { return () => {
const { prefixCls, hasSider, tagName } = props; const { prefixCls, hasSider, tagName } = props;
const divCls = classNames(prefixCls, { const divCls = classNames(prefixCls, {
[`${prefixCls}-has-sider`]: [`${prefixCls}-has-sider`]:
typeof hasSider === 'boolean' ? hasSider : siders.value.length > 0, typeof hasSider === 'boolean' ? hasSider : siders.value.length > 0,
[`${prefixCls}-rtl`]: direction.value === 'rtl',
}); });
return createVNode(tagName, { class: divCls }, slots.default?.()); return createVNode(tagName, { class: divCls }, slots.default?.());
}; };

View File

@ -2,6 +2,7 @@
@import '../../style/mixins/index'; @import '../../style/mixins/index';
@layout-prefix-cls: ~'@{ant-prefix}-layout'; @layout-prefix-cls: ~'@{ant-prefix}-layout';
@layout-menu-prefix-cls: ~'@{ant-prefix}-menu';
.@{layout-prefix-cls} { .@{layout-prefix-cls} {
display: flex; display: flex;
@ -18,9 +19,10 @@
&&-has-sider { &&-has-sider {
flex-direction: row; flex-direction: row;
> .@{layout-prefix-cls}, > .@{layout-prefix-cls},
> .@{layout-prefix-cls}-content { > .@{layout-prefix-cls}-content {
overflow-x: hidden; width: 0; // https://segmentfault.com/a/1190000019498300
} }
} }
@ -32,6 +34,7 @@
&-header { &-header {
height: @layout-header-height; height: @layout-header-height;
padding: @layout-header-padding; padding: @layout-header-padding;
color: @layout-header-color;
line-height: @layout-header-height; line-height: @layout-header-height;
background: @layout-header-background; background: @layout-header-background;
} }
@ -64,6 +67,10 @@
// https://github.com/ant-design/ant-design/issues/7967 // https://github.com/ant-design/ant-design/issues/7967
// solution from https://stackoverflow.com/a/33132624/3040605 // solution from https://stackoverflow.com/a/33132624/3040605
padding-top: 0.1px; padding-top: 0.1px;
.@{layout-menu-prefix-cls}.@{layout-menu-prefix-cls}-inline-collapsed {
width: auto;
}
} }
&-has-trigger { &-has-trigger {
@ -88,7 +95,7 @@
} }
&-zero-width { &-zero-width {
& > * { > * {
overflow: hidden; overflow: hidden;
} }
@ -108,8 +115,19 @@
cursor: pointer; cursor: pointer;
transition: background 0.3s ease; transition: background 0.3s ease;
&:hover { &::after {
background: tint(@layout-sider-background, 10%); position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: transparent;
transition: all 0.3s;
content: '';
}
&:hover::after {
background: rgba(255, 255, 255, 0.1);
} }
&-right { &-right {
@ -122,3 +140,4 @@
} }
@import './light'; @import './light';
@import './rtl';

View File

@ -1,15 +1,11 @@
.@{layout-prefix-cls} { .@{layout-prefix-cls}-sider-light {
&-sider { background: @layout-sider-background-light;
&-light { .@{layout-prefix-cls}-sider-trigger {
background: @layout-sider-background-light; color: @layout-trigger-color-light;
} background: @layout-trigger-background-light;
&-light &-trigger { }
color: @layout-trigger-color-light; .@{layout-prefix-cls}-sider-zero-width-trigger {
background: @layout-trigger-background-light; color: @layout-trigger-color-light;
} background: @layout-trigger-background-light;
&-light &-zero-width-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
} }
} }

View File

@ -0,0 +1,10 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@layout-prefix-cls: ~'@{ant-prefix}-layout';
.@{layout-prefix-cls} {
&-rtl {
direction: rtl;
}
}

View File

@ -27,6 +27,7 @@ import {
import devWarning from '../../vc-util/devWarning'; import devWarning from '../../vc-util/devWarning';
import { collapseMotion, CSSMotionProps } from '../../_util/transition'; import { collapseMotion, CSSMotionProps } from '../../_util/transition';
import uniq from 'lodash-es/uniq'; import uniq from 'lodash-es/uniq';
import { SiderCollapsedKey } from '../../layout/injectionKey';
export const menuProps = { export const menuProps = {
prefixCls: String, prefixCls: String,
@ -72,10 +73,7 @@ export default defineComponent({
setup(props, { slots, emit }) { setup(props, { slots, emit }) {
const { prefixCls, direction } = useConfigInject('menu', props); const { prefixCls, direction } = useConfigInject('menu', props);
const store = reactive<Record<string, StoreMenuInfo>>({}); const store = reactive<Record<string, StoreMenuInfo>>({});
const siderCollapsed = inject( const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
'layoutSiderCollapsed',
computed(() => undefined),
);
const inlineCollapsed = computed(() => { const inlineCollapsed = computed(() => {
if (siderCollapsed.value !== undefined) { if (siderCollapsed.value !== undefined) {
return siderCollapsed.value; return siderCollapsed.value;

View File

@ -300,10 +300,11 @@
// Layout // Layout
@layout-body-background: #f0f2f5; @layout-body-background: #f0f2f5;
@layout-header-background: #001529; @layout-header-background: #001529;
@layout-footer-background: @layout-body-background;
@layout-header-height: 64px; @layout-header-height: 64px;
@layout-header-padding: 0 50px; @layout-header-padding: 0 50px;
@layout-header-color: @text-color;
@layout-footer-padding: 24px 50px; @layout-footer-padding: 24px 50px;
@layout-footer-background: @layout-body-background;
@layout-sider-background: @layout-header-background; @layout-sider-background: @layout-header-background;
@layout-trigger-height: 48px; @layout-trigger-height: 48px;
@layout-trigger-background: #002140; @layout-trigger-background: #002140;