diff --git a/components/layout/Sider.tsx b/components/layout/Sider.tsx index 415340613..9b7706293 100644 --- a/components/layout/Sider.tsx +++ b/components/layout/Sider.tsx @@ -1,17 +1,25 @@ 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 { tuple } from '../_util/type'; -import { getOptionProps, hasProp, getComponent, getSlot } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; -import BaseMixin from '../_util/BaseMixin'; import isNumeric from '../_util/isNumeric'; -import { defaultConfigProvider } from '../config-provider'; import BarsOutlined from '@ant-design/icons-vue/BarsOutlined'; import RightOutlined from '@ant-design/icons-vue/RightOutlined'; import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; -import omit from 'omit.js'; -import { SiderHookProvider } from './layout'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import { SiderCollapsedKey, SiderHookProviderKey } from './injectionKey'; const dimensionMaxMap = { xs: '479.98px', @@ -24,7 +32,7 @@ const dimensionMaxMap = { export type CollapseType = 'clickTrigger' | 'responsive'; -export const SiderProps = { +export const siderProps = { prefixCls: PropTypes.string, collapsible: PropTypes.looseBool, collapsed: PropTypes.looseBool, @@ -40,6 +48,7 @@ export const SiderProps = { onCollapse: Function as PropType<(collapsed: boolean, type: CollapseType) => void>, }; +export type SiderProps = Partial>; // export interface SiderState { // collapsed?: boolean; // below: boolean; @@ -61,10 +70,8 @@ const generateId = (() => { export default defineComponent({ name: 'ALayoutSider', - mixins: [BaseMixin], inheritAttrs: false, - __ANT_LAYOUT_SIDER: true, - props: initDefaultProps(SiderProps, { + props: initDefaultProps(siderProps, { collapsible: false, defaultCollapsed: false, reverseArrow: false, @@ -72,173 +79,141 @@ export default defineComponent({ collapsedWidth: 80, }), emits: ['breakpoint', 'update:collapsed', 'collapse'], - setup() { - return { - siderHook: inject('siderHook', {}), - configProvider: inject('configProvider', defaultConfigProvider), - }; - }, - 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 ? ( - - - - ) : null; - const iconObj = { - expanded: reverseArrow ? : , - collapsed: reverseArrow ? : , - }; - const status = this.sCollapsed ? 'collapsed' : 'expanded'; - const defaultTrigger = iconObj[status]; - const triggerDom = - trigger !== null - ? zeroWidthTrigger || ( -
- {trigger || defaultTrigger} -
- ) - : 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 ( - + setup(props, { emit, attrs, slots }) { + const { prefixCls } = useConfigInject('layout-sider', props); + const siderHook = inject(SiderHookProviderKey); + const collapsed = ref( + !!(props.collapsed !== undefined ? props.collapsed : props.defaultCollapsed), ); + 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 ? ( + + {trigger || } + + ) : null; + const iconObj = { + expanded: reverseArrow ? : , + collapsed: reverseArrow ? : , + }; + const status = collapsed.value ? 'collapsed' : 'expanded'; + const defaultTrigger = iconObj[status]; + const triggerDom = + trigger !== null + ? zeroWidthTrigger || ( +
+ {trigger || defaultTrigger} +
+ ) + : 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 ( + + ); + }; }, }); diff --git a/components/layout/index.ts b/components/layout/index.ts index a9b01dcf3..000f47ea7 100644 --- a/components/layout/index.ts +++ b/components/layout/index.ts @@ -2,6 +2,9 @@ import { App, Plugin } from 'vue'; import Layout from './layout'; import Sider from './Sider'; +export { BasicProps as LayoutProps } from './layout'; +export { SiderProps } from './Sider'; + Layout.Sider = Sider; /* istanbul ignore next */ diff --git a/components/layout/injectionKey.ts b/components/layout/injectionKey.ts new file mode 100644 index 000000000..d827c52f7 --- /dev/null +++ b/components/layout/injectionKey.ts @@ -0,0 +1,12 @@ +import { Ref, InjectionKey } from 'vue'; + +export type SiderCollapsed = Ref; + +export const SiderCollapsedKey: InjectionKey = Symbol('siderCollapsed'); + +export interface SiderHookProvider { + addSider?: (id: string) => void; + removeSider?: (id: string) => void; +} + +export const SiderHookProviderKey: InjectionKey = Symbol('siderHookProvider'); diff --git a/components/layout/layout.tsx b/components/layout/layout.tsx index 8629cc1e9..6c79bbf36 100644 --- a/components/layout/layout.tsx +++ b/components/layout/layout.tsx @@ -1,17 +1,8 @@ -import { - createVNode, - defineComponent, - inject, - provide, - toRefs, - ref, - ExtractPropTypes, - HTMLAttributes, -} from 'vue'; +import { createVNode, defineComponent, provide, ref, ExtractPropTypes, HTMLAttributes } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; -import { defaultConfigProvider } from '../config-provider'; -import { flattenChildren } from '../_util/props-util'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import { SiderHookProviderKey } from './injectionKey'; export const basicProps = { prefixCls: PropTypes.string, @@ -21,40 +12,29 @@ export const basicProps = { export type BasicProps = Partial> & HTMLAttributes; -export interface SiderHookProvider { - addSider?: (id: string) => void; - removeSider?: (id: string) => void; -} - type GeneratorArgument = { suffixCls: string; - tagName: string; + tagName: 'header' | 'footer' | 'main' | 'section'; name: string; }; function generator({ suffixCls, tagName, name }: GeneratorArgument) { return (BasicComponent: typeof Basic) => { - const Adapter = defineComponent({ + const Adapter = defineComponent({ name, + props: basicProps, setup(props, { slots }) { - const { getPrefixCls } = inject('configProvider', defaultConfigProvider); + const { prefixCls } = useConfigInject(suffixCls, props); return () => { - const { prefixCls: customizePrefixCls } = props; - const prefixCls = getPrefixCls(suffixCls, customizePrefixCls); const basicComponentProps = { - prefixCls, - ...props, + prefixCls: prefixCls.value, tagName, + ...props, }; - return ( - - {flattenChildren(slots.default?.())} - - ); + return {slots.default?.()}; }; }, }); - Adapter.props = basicProps; return Adapter; }; } @@ -62,30 +42,32 @@ function generator({ suffixCls, tagName, name }: GeneratorArgument) { const Basic = defineComponent({ props: basicProps, setup(props, { slots }) { - const { prefixCls, tagName } = toRefs(props); - return () => createVNode(tagName.value, { class: prefixCls.value }, slots.default?.()); + return () => createVNode(props.tagName, { class: props.prefixCls }, slots.default?.()); }, }); const BasicLayout = defineComponent({ props: basicProps, setup(props, { slots }) { + const { direction } = useConfigInject('', props); const siders = ref([]); - const siderHookProvider: SiderHookProvider = { - addSider: id => { + const siderHookProvider = { + addSider: (id: string) => { siders.value = [...siders.value, id]; }, - removeSider: id => { + removeSider: (id: string) => { siders.value = siders.value.filter(currentId => currentId !== id); }, }; - provide('siderHook', siderHookProvider); + + provide(SiderHookProviderKey, siderHookProvider); return () => { const { prefixCls, hasSider, tagName } = props; const divCls = classNames(prefixCls, { [`${prefixCls}-has-sider`]: typeof hasSider === 'boolean' ? hasSider : siders.value.length > 0, + [`${prefixCls}-rtl`]: direction.value === 'rtl', }); return createVNode(tagName, { class: divCls }, slots.default?.()); }; diff --git a/components/layout/style/index.less b/components/layout/style/index.less index 131716f58..86a912e70 100644 --- a/components/layout/style/index.less +++ b/components/layout/style/index.less @@ -2,6 +2,7 @@ @import '../../style/mixins/index'; @layout-prefix-cls: ~'@{ant-prefix}-layout'; +@layout-menu-prefix-cls: ~'@{ant-prefix}-menu'; .@{layout-prefix-cls} { display: flex; @@ -18,9 +19,10 @@ &&-has-sider { flex-direction: row; + > .@{layout-prefix-cls}, > .@{layout-prefix-cls}-content { - overflow-x: hidden; + width: 0; // https://segmentfault.com/a/1190000019498300 } } @@ -32,6 +34,7 @@ &-header { height: @layout-header-height; padding: @layout-header-padding; + color: @layout-header-color; line-height: @layout-header-height; background: @layout-header-background; } @@ -64,6 +67,10 @@ // https://github.com/ant-design/ant-design/issues/7967 // solution from https://stackoverflow.com/a/33132624/3040605 padding-top: 0.1px; + + .@{layout-menu-prefix-cls}.@{layout-menu-prefix-cls}-inline-collapsed { + width: auto; + } } &-has-trigger { @@ -88,7 +95,7 @@ } &-zero-width { - & > * { + > * { overflow: hidden; } @@ -108,8 +115,19 @@ cursor: pointer; transition: background 0.3s ease; - &:hover { - background: tint(@layout-sider-background, 10%); + &::after { + 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 { @@ -122,3 +140,4 @@ } @import './light'; +@import './rtl'; diff --git a/components/layout/style/light.less b/components/layout/style/light.less index bf9e53d93..35d636df1 100644 --- a/components/layout/style/light.less +++ b/components/layout/style/light.less @@ -1,15 +1,11 @@ -.@{layout-prefix-cls} { - &-sider { - &-light { - background: @layout-sider-background-light; - } - &-light &-trigger { - color: @layout-trigger-color-light; - background: @layout-trigger-background-light; - } - &-light &-zero-width-trigger { - color: @layout-trigger-color-light; - background: @layout-trigger-background-light; - } +.@{layout-prefix-cls}-sider-light { + background: @layout-sider-background-light; + .@{layout-prefix-cls}-sider-trigger { + color: @layout-trigger-color-light; + background: @layout-trigger-background-light; + } + .@{layout-prefix-cls}-sider-zero-width-trigger { + color: @layout-trigger-color-light; + background: @layout-trigger-background-light; } } diff --git a/components/layout/style/rtl.less b/components/layout/style/rtl.less new file mode 100644 index 000000000..da7aca0dd --- /dev/null +++ b/components/layout/style/rtl.less @@ -0,0 +1,10 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@layout-prefix-cls: ~'@{ant-prefix}-layout'; + +.@{layout-prefix-cls} { + &-rtl { + direction: rtl; + } +} diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 9ec9711dd..5d8fa6408 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -27,6 +27,7 @@ import { import devWarning from '../../vc-util/devWarning'; import { collapseMotion, CSSMotionProps } from '../../_util/transition'; import uniq from 'lodash-es/uniq'; +import { SiderCollapsedKey } from '../../layout/injectionKey'; export const menuProps = { prefixCls: String, @@ -72,10 +73,7 @@ export default defineComponent({ setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const store = reactive>({}); - const siderCollapsed = inject( - 'layoutSiderCollapsed', - computed(() => undefined), - ); + const siderCollapsed = inject(SiderCollapsedKey, ref(undefined)); const inlineCollapsed = computed(() => { if (siderCollapsed.value !== undefined) { return siderCollapsed.value; diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 45787fea5..5dca3a0fc 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -300,10 +300,11 @@ // Layout @layout-body-background: #f0f2f5; @layout-header-background: #001529; -@layout-footer-background: @layout-body-background; @layout-header-height: 64px; @layout-header-padding: 0 50px; +@layout-header-color: @text-color; @layout-footer-padding: 24px 50px; +@layout-footer-background: @layout-body-background; @layout-sider-background: @layout-header-background; @layout-trigger-height: 48px; @layout-trigger-background: #002140;