diff --git a/components/_util/vue-types/index.ts b/components/_util/vue-types/index.ts index a08983ba7..9b0c05261 100644 --- a/components/_util/vue-types/index.ts +++ b/components/_util/vue-types/index.ts @@ -36,9 +36,7 @@ export function withUndefined(type: T): T { } export default PropTypes as VueTypesInterface & { - readonly looseBool: VueTypeValidableDef & { - default: undefined; - }; + readonly looseBool: VueTypeValidableDef; readonly style: VueTypeValidableDef; readonly VNodeChild: VueTypeValidableDef; }; diff --git a/components/layout/Sider.jsx b/components/layout/Sider.tsx similarity index 81% rename from components/layout/Sider.jsx rename to components/layout/Sider.tsx index 2edd235f9..b130fe359 100644 --- a/components/layout/Sider.jsx +++ b/components/layout/Sider.tsx @@ -1,13 +1,9 @@ import classNames from '../_util/classNames'; -import { inject, provide } from 'vue'; +import { inject, provide, PropType, defineComponent } from 'vue'; import PropTypes from '../_util/vue-types'; -import { - initDefaultProps, - getOptionProps, - hasProp, - getComponent, - getSlot, -} from '../_util/props-util'; +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'; @@ -15,6 +11,7 @@ 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'; const dimensionMaxMap = { xs: '479.98px', @@ -25,7 +22,7 @@ const dimensionMaxMap = { xxl: '1599.98px', }; -// export type CollapseType = 'clickTrigger' | 'responsive'; +export type CollapseType = 'clickTrigger' | 'responsive'; export const SiderProps = { prefixCls: PropTypes.string, @@ -33,16 +30,14 @@ export const SiderProps = { collapsed: PropTypes.looseBool, defaultCollapsed: PropTypes.looseBool, reverseArrow: PropTypes.looseBool, - // onCollapse?: (collapsed: boolean, type: CollapseType) => void; - zeroWidthTriggerStyle: PropTypes.object, - trigger: PropTypes.any, + zeroWidthTriggerStyle: PropTypes.style, + trigger: PropTypes.VNodeChild, width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), collapsedWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - breakpoint: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', 'xxl']), - theme: PropTypes.oneOf(['light', 'dark']).def('dark'), - onBreakpoint: PropTypes.func, - onCollapse: PropTypes.func, - 'onUpdate:collapsed': PropTypes.func, + breakpoint: PropTypes.oneOf(tuple('xs', 'sm', 'md', 'lg', 'xl', 'xxl')), + theme: PropTypes.oneOf(tuple('light', 'dark')).def('dark'), + onBreakpoint: Function as PropType<(broken: boolean) => void>, + onCollapse: Function as PropType<(collapsed: boolean, type: CollapseType) => void>, }; // export interface SiderState { @@ -63,7 +58,7 @@ const generateId = (() => { }; })(); -export default { +export default defineComponent({ name: 'ALayoutSider', __ANT_LAYOUT_SIDER: true, mixins: [BaseMixin], @@ -74,17 +69,19 @@ export default { width: 200, collapsedWidth: 80, }), + emits: ['breakpoint', 'update:collapsed', 'collapse'], data() { - this.uniqueId = generateId('ant-sider-'); - let matchMedia; + const uniqueId = generateId('ant-sider-'); + let matchMedia: typeof window.matchMedia; if (typeof window !== 'undefined') { matchMedia = window.matchMedia; } - const props = getOptionProps(this); + const props = getOptionProps(this) as any; + let mql: MediaQueryList; if (matchMedia && props.breakpoint && props.breakpoint in dimensionMaxMap) { - this.mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`); + mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`); } - let sCollapsed; + let sCollapsed: boolean; if ('collapsed' in props) { sCollapsed = props.collapsed; } else { @@ -94,6 +91,8 @@ export default { sCollapsed, below: false, belowShow: false, + uniqueId, + mql, }; }, watch: { @@ -108,7 +107,7 @@ export default { }, setup() { return { - siderHook: inject('siderHook', {}), + siderHook: inject('siderHook', {}), configProvider: inject('configProvider', defaultConfigProvider), }; }, @@ -136,7 +135,7 @@ export default { } }, methods: { - responsiveHandler(mql) { + responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) { this.setState({ below: mql.matches }); this.$emit('breakpoint', mql.matches); if (this.sCollapsed !== mql.matches) { @@ -144,7 +143,7 @@ export default { } }, - setCollapsed(collapsed, type) { + setCollapsed(collapsed: boolean, type: CollapseType) { if (!hasProp(this, 'collapsed')) { this.setState({ sCollapsed: collapsed, @@ -176,7 +175,7 @@ export default { collapsedWidth, zeroWidthTriggerStyle, ...others - } = { ...getOptionProps(this), ...this.$attrs }; + } = { ...getOptionProps(this), ...this.$attrs } as any; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('layout-sider', customizePrefixCls); const divProps = omit(others, [ @@ -188,7 +187,6 @@ export default { 'siderHook', 'zeroWidthTriggerStyle', 'trigger', - 'onUpdate:collapse', ]); const trigger = getComponent(this, 'trigger'); const rawWidth = this.sCollapsed ? collapsedWidth : width; @@ -241,4 +239,4 @@ export default { ); }, -}; +}); diff --git a/components/layout/index.js b/components/layout/index.ts similarity index 72% rename from components/layout/index.js rename to components/layout/index.ts index bdfbb3339..3219128bd 100644 --- a/components/layout/index.js +++ b/components/layout/index.ts @@ -1,10 +1,11 @@ +import { App } from 'vue'; import Layout from './layout'; import Sider from './Sider'; Layout.Sider = Sider; /* istanbul ignore next */ -Layout.install = function(app) { +Layout.install = function(app: App) { app.component(Layout.name, Layout); app.component(Layout.Header.name, Layout.Header); app.component(Layout.Footer.name, Layout.Footer); @@ -12,4 +13,6 @@ Layout.install = function(app) { app.component(Layout.Content.name, Layout.Content); return app; }; -export default Layout; +export default Layout as typeof Layout & { + readonly Sider: typeof Sider; +}; diff --git a/components/layout/layout.jsx b/components/layout/layout.jsx deleted file mode 100644 index bed7173c1..000000000 --- a/components/layout/layout.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { inject, provide } from 'vue'; -import PropTypes from '../_util/vue-types'; -import classNames from '../_util/classNames'; -import { getOptionProps, getSlot } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; - -export const BasicProps = { - prefixCls: PropTypes.string, - hasSider: PropTypes.looseBool, - tagName: PropTypes.string, -}; - -function generator({ suffixCls, tagName, name }) { - return BasicComponent => { - return { - name, - props: BasicComponent.props, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), - }; - }, - render() { - const { prefixCls: customizePrefixCls } = this.$props; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls(suffixCls, customizePrefixCls); - - const basicComponentProps = { - prefixCls, - ...getOptionProps(this), - tagName, - }; - return {getSlot(this)}; - }, - }; - }; -} - -const Basic = { - props: BasicProps, - render() { - const { prefixCls, tagName: Tag } = this; - const divProps = { - class: prefixCls, - }; - return {getSlot(this)}; - }, -}; - -const BasicLayout = { - props: BasicProps, - data() { - return { - siders: [], - }; - }, - created() { - provide('siderHook', { - addSider: id => { - this.siders = [...this.siders, id]; - }, - removeSider: id => { - this.siders = this.siders.filter(currentId => currentId !== id); - }, - }); - }, - render() { - const { prefixCls, hasSider, tagName: Tag } = this; - const divCls = classNames(prefixCls, { - [`${prefixCls}-has-sider`]: typeof hasSider === 'boolean' ? hasSider : this.siders.length > 0, - }); - const divProps = { - class: divCls, - }; - return {getSlot(this)}; - }, -}; - -const Layout = generator({ - suffixCls: 'layout', - tagName: 'section', - name: 'ALayout', -})(BasicLayout); - -const Header = generator({ - suffixCls: 'layout-header', - tagName: 'header', - name: 'ALayoutHeader', -})(Basic); - -const Footer = generator({ - suffixCls: 'layout-footer', - tagName: 'footer', - name: 'ALayoutFooter', -})(Basic); - -const Content = generator({ - suffixCls: 'layout-content', - tagName: 'main', - name: 'ALayoutContent', -})(Basic); - -Layout.Header = Header; -Layout.Footer = Footer; -Layout.Content = Content; - -export default Layout; diff --git a/components/layout/layout.tsx b/components/layout/layout.tsx new file mode 100644 index 000000000..2e56c75ca --- /dev/null +++ b/components/layout/layout.tsx @@ -0,0 +1,110 @@ +import { createVNode, defineComponent, inject, provide, toRefs, ref } from 'vue'; +import PropTypes from '../_util/vue-types'; +import classNames from '../_util/classNames'; +import { defaultConfigProvider } from '../config-provider'; + +export const BasicProps = { + prefixCls: PropTypes.string, + hasSider: PropTypes.looseBool, + tagName: PropTypes.string, +}; + +export interface SiderHookProvider { + addSider?: (id: string) => void; + removeSider?: (id: string) => void; +} + +type GeneratorArgument = { + suffixCls: string; + tagName: string; + name: string; +}; + +function generator({ suffixCls, tagName, name }: GeneratorArgument) { + return (BasicComponent: typeof Basic) => { + return defineComponent({ + name, + props: BasicProps, + setup(props, { slots }) { + const { getPrefixCls } = inject('configProvider', defaultConfigProvider); + return () => { + const { prefixCls: customizePrefixCls } = props; + const prefixCls = getPrefixCls(suffixCls, customizePrefixCls); + const basicComponentProps = { + prefixCls, + ...props, + tagName, + }; + return {slots.default?.()}; + }; + }, + }); + }; +} + +const Basic = defineComponent({ + props: BasicProps, + setup(props, { slots }) { + const { prefixCls, tagName } = toRefs(props); + return () => createVNode(tagName.value, { class: prefixCls.value }, slots.default?.()); + }, +}); + +const BasicLayout = defineComponent({ + props: BasicProps, + setup(props, { slots }) { + const siders = ref([]); + const siderHookProvider: SiderHookProvider = { + addSider: id => { + siders.value = [...siders.value, id]; + }, + removeSider: id => { + siders.value = siders.value.filter(currentId => currentId !== id); + }, + }; + provide('siderHook', siderHookProvider); + + return () => { + const { prefixCls, hasSider, tagName } = props; + const divCls = classNames(prefixCls, { + [`${prefixCls}-has-sider`]: + typeof hasSider === 'boolean' ? hasSider : siders.value.length > 0, + }); + return createVNode(tagName, { class: divCls }, slots.default?.()); + }; + }, +}); + +const Layout = generator({ + suffixCls: 'layout', + tagName: 'section', + name: 'ALayout', +})(BasicLayout); + +const Header = generator({ + suffixCls: 'layout-header', + tagName: 'header', + name: 'ALayoutHeader', +})(Basic); + +const Footer = generator({ + suffixCls: 'layout-footer', + tagName: 'footer', + name: 'ALayoutFooter', +})(Basic); + +const Content = generator({ + suffixCls: 'layout-content', + tagName: 'main', + name: 'ALayoutContent', +})(Basic); + +Layout.Header = Header; +Layout.Footer = Footer; +Layout.Content = Content; + +export default Layout as typeof Layout & { + readonly Header: typeof Header; + readonly Footer: typeof Footer; + readonly Content: typeof Content; +}; diff --git a/components/layout/style/index.js b/components/layout/style/index.ts similarity index 100% rename from components/layout/style/index.js rename to components/layout/style/index.ts