diff --git a/apps/playground/src/pages/flex/basic.vue b/apps/playground/src/pages/flex/basic.vue new file mode 100644 index 000000000..32c06fccb --- /dev/null +++ b/apps/playground/src/pages/flex/basic.vue @@ -0,0 +1,93 @@ + + + diff --git a/packages/ui/src/components/flex/Flex.vue b/packages/ui/src/components/flex/Flex.vue new file mode 100644 index 000000000..4aeb22e50 --- /dev/null +++ b/packages/ui/src/components/flex/Flex.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap b/packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000..048662e1a --- /dev/null +++ b/packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Flex > should render correctly 1`] = ` +"
+
test
+
" +`; diff --git a/packages/ui/src/components/flex/__tests__/index.test.ts b/packages/ui/src/components/flex/__tests__/index.test.ts new file mode 100644 index 000000000..83d7686f2 --- /dev/null +++ b/packages/ui/src/components/flex/__tests__/index.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it, vi } from 'vitest' +import { Flex } from '@ant-design-vue/ui' +import { mount } from '@vue/test-utils' + +describe('Flex', () => { + it('should render correctly', () => { + const wrapper = mount(Flex, { + slots: { + default: `
test
`, + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('Flex', () => { + const wrapper = mount(Flex, { + props: { + justify: 'center' + }, + slots: { + default: `
test
` + }, + }); + + const wrapper3 = mount(Flex, { + props: { + flex: '0 1 auto', + }, + slots: { + default: `
test
`, + }, + }); + + expect(wrapper.classes('ant-flex')).toBeTruthy(); + expect(wrapper.find('.ant-flex-justify-center')).toBeTruthy(); + expect(wrapper3.classes('ant-flex')).toBeTruthy(); + expect(wrapper3.element.style.flex).toBe('0 1 auto'); + }); + + describe('Props: gap', () => { + it('support string', () => { + const wrapper = mount(Flex, { + props: { + gap: 'inherit', + }, + slots: { + default: `
test
`, + }, + }); + expect(wrapper.classes('ant-flex')).toBeTruthy(); + expect(wrapper.element.style.gap).toBe('inherit'); + }); + + it('support number', () => { + const wrapper = mount(Flex, { + props: { + gap: '100', + }, + slots: { + default: `
test
`, + }, + }); + expect(wrapper.classes('ant-flex')).toBeTruthy(); + expect(wrapper.element.style.gap).toBe('100px'); + }); + + it('support preset size', () => { + const wrapper = mount(Flex, { + props: { + gap: 'small', + }, + slots: { + default: `
test
`, + }, + }); + + expect(wrapper.classes('ant-flex')).toBeTruthy(); + expect(wrapper.classes('ant-flex-gap-small')).toBeTruthy(); + }); + }); + + it('Component work', () => { + const wrapper = mount(Flex, { + slots: { + default: `
test
` + }, + }); + + const wrapper2 = mount(Flex, { + props: { + componentTag: 'span' + }, + slots: { + default: `
test
` + }, + }); + + expect(wrapper.find('.ant-flex').element.tagName).toBe('DIV'); + expect(wrapper2.find('.ant-flex').element.tagName).toBe('SPAN'); + }); + + it('when vertical=true should stretch work', () => { + const wrapper = mount(Flex, { + props: { + vertical: true + }, + slots: { + default: `
test
` + }, + }); + + const wrapper2 = mount(Flex, { + props: { + vertical: true, + align: 'center', + }, + slots: { + default: `
test
`, + }, + }); + + expect(wrapper.find('.ant-flex-align-stretch')).toBeTruthy(); + expect(wrapper2.find('.ant-flex-align-center')).toBeTruthy(); + }); + + it('wrap prop shouled support boolean', () => { + const wrapper = mount(Flex, { + props: { + wrap: 'wrap', + }, + slots: { + default: `
test
`, + }, + }); + + const wrapper2 = mount(Flex, { + props: { + wrap: true, + }, + slots: { + default: `
test
`, + }, + }); + + expect(wrapper.classes('ant-flex-wrap-wrap')).toBeTruthy(); + expect(wrapper2.classes('ant-flex-wrap-wrap')).toBeTruthy(); + }) +}) diff --git a/packages/ui/src/components/flex/index.ts b/packages/ui/src/components/flex/index.ts new file mode 100644 index 000000000..c6b83d076 --- /dev/null +++ b/packages/ui/src/components/flex/index.ts @@ -0,0 +1,14 @@ +import { App, Plugin } from 'vue' +import Flex from './Flex.vue' +import './style/index.css' + + +export { default as Flex } from './Flex.vue' +export * from './meta' + +Flex.install = function (app: App) { + app.component('AFlex', Flex) + return app +} + +export default Flex as typeof Flex & Plugin diff --git a/packages/ui/src/components/flex/meta.ts b/packages/ui/src/components/flex/meta.ts new file mode 100644 index 000000000..dc7d8ed54 --- /dev/null +++ b/packages/ui/src/components/flex/meta.ts @@ -0,0 +1,20 @@ +import { CSSProperties } from "vue" + +type SizeType = 'small' | 'middle' | 'large' | undefined + +export type FlexProps = { + prefixCls?: string + rootClassName?: string + vertical?: boolean + wrap?: CSSProperties['flexWrap'] | boolean + justify?: CSSProperties['justifyContent'] + align?: CSSProperties['alignItems'] + flex?: CSSProperties['flex'] + gap?: CSSProperties['gap'] | SizeType + componentTag?: any +} + +export const flexDefaultProps = { + prefixCls: 'ant-flex', + componentTag: 'div', +} as const diff --git a/packages/ui/src/components/flex/style/index.css b/packages/ui/src/components/flex/style/index.css new file mode 100644 index 000000000..b7bcf7ac6 --- /dev/null +++ b/packages/ui/src/components/flex/style/index.css @@ -0,0 +1,95 @@ +@reference '../../../style/tailwind.css'; + +.ant-flex { + @apply flex; + @apply m-0; + @apply p-0; + &:where(.ant-flex-vertical) { + @apply flex-col; + } + &:where(.ant-flex-rtl) { + @apply flex-row-reverse; + } + /* gap */ + &:where(.ant-flex-gap-small) { + @apply gap-[8px]; + } + &:where(.ant-flex-gap-middle) { + @apply gap-[16px]; + } + &:where(.ant-flex-gap-large) { + @apply gap-[32px]; + } + /* wrap */ + &:where(.ant-flex-wrap-wrap) { + @apply flex-wrap; + } + &:where(.ant-flex-wrap-nowrap) { + @apply flex-nowrap; + } + &:where(.ant-flex-wrap-wrap-reverse) { + @apply flex-wrap-reverse; + } + /* align */ + &:where(.ant-flex-align-center) { + @apply items-center; + } + &:where(.ant-flex-align-start) { + @apply items-start; + } + &:where(.ant-flex-align-end) { + @apply items-end; + } + &:where(.ant-flex-align-flex-start) { + @apply items-start; + } + &:where(.ant-flex-align-flex-end) { + @apply items-end; + } + &:where(.ant-flex-align-self-start) { + @apply self-start; + } + &:where(.ant-flex-align-self-end) { + @apply self-end; + } + &:where(.ant-flex-align-baseline) { + @apply items-baseline; + } + &:where(.ant-flex-align-normal) { + @apply content-normal; + } + &:where(.ant-flex-align-stretch) { + @apply items-stretch; + } + /* justify */ + &:where(.ant-flex-justify-flex-start) { + @apply justify-start; + } + &:where(.ant-flex-justify-flex-end) { + @apply justify-end; + } + &:where(.ant-flex-justify-start) { + @apply justify-start; + } + &:where(.ant-flex-justify-end) { + @apply justify-end; + } + &:where(.ant-flex-justify-center) { + @apply justify-center; + } + &:where(.ant-flex-justify-space-between) { + @apply justify-between; + } + &:where(.ant-flex-justify-space-around) { + @apply justify-around; + } + &:where(.ant-flex-justify-space-evenly) { + @apply justify-evenly; + } + &:where(.ant-flex-justify-stretch) { + @apply justify-stretch; + } + &:where(.ant-flex-justify-normal) { + @apply justify-normal; + } +} diff --git a/packages/ui/src/components/flex/utils.ts b/packages/ui/src/components/flex/utils.ts new file mode 100644 index 000000000..d3de11011 --- /dev/null +++ b/packages/ui/src/components/flex/utils.ts @@ -0,0 +1,70 @@ +import classNames from '../../utils/classNames' + +import type { FlexProps } from './meta' + +export const flexWrapValues = ['wrap', 'nowrap', 'wrap-reverse'] as const + +export const justifyContentValues = [ + 'flex-start', + 'flex-end', + 'start', + 'end', + 'center', + 'space-between', + 'space-around', + 'space-evenly', + 'stretch', + 'normal', + 'left', + 'right', +] as const + +export const alignItemsValues = [ + 'center', + 'start', + 'end', + 'flex-start', + 'flex-end', + 'self-start', + 'self-end', + 'baseline', + 'normal', + 'stretch', +] as const + +const genClsWrap = (prefixCls: string, props: FlexProps) => { + const wrapCls: Record = {} + flexWrapValues.forEach(cssKey => { + // Handle both boolean attribute (wrap="wrap") and string value (wrap="wrap") + const isMatch = props.wrap === true && cssKey === 'wrap' || props.wrap === cssKey + wrapCls[`${prefixCls}-wrap-${cssKey}`] = isMatch + }) + return wrapCls +} + +const genClsAlign = (prefixCls: string, props: FlexProps) => { + const alignCls: Record = {} + alignItemsValues.forEach(cssKey => { + alignCls[`${prefixCls}-align-${cssKey}`] = props.align === cssKey + }) + alignCls[`${prefixCls}-align-stretch`] = !props.align && !!props.vertical + return alignCls +} + +const genClsJustify = (prefixCls: string, props: FlexProps) => { + const justifyCls: Record = {} + justifyContentValues.forEach(cssKey => { + justifyCls[`${prefixCls}-justify-${cssKey}`] = props.justify === cssKey + }) + return justifyCls +} + +function createFlexClassNames(prefixCls: string, props: FlexProps) { + return classNames({ + ...genClsWrap(prefixCls, props), + ...genClsAlign(prefixCls, props), + ...genClsJustify(prefixCls, props), + }) +} + +export default createFlexClassNames diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 6bb401da3..a2778c066 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -2,3 +2,4 @@ export { default as Button } from './button' export { default as Input } from './input' export { default as Theme } from './theme' export { default as Affix } from './affix' +export { default as Flex } from './flex' diff --git a/packages/ui/src/utils/classNames.ts b/packages/ui/src/utils/classNames.ts new file mode 100644 index 000000000..4ccf120db --- /dev/null +++ b/packages/ui/src/utils/classNames.ts @@ -0,0 +1,27 @@ +import { isArray, isString, isObject } from './util' +function classNames(...args: any[]) { + const classes = [] + for (let i = 0; i < args.length; i++) { + const value = args[i] + if (!value) continue + if (isString(value)) { + classes.push(value) + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const inner = classNames(value[i]) + if (inner) { + classes.push(inner) + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + classes.push(name) + } + } + } + } + return classes.join(' ') +} + +export default classNames diff --git a/packages/ui/src/utils/gapSize.ts b/packages/ui/src/utils/gapSize.ts new file mode 100644 index 000000000..c9a40af95 --- /dev/null +++ b/packages/ui/src/utils/gapSize.ts @@ -0,0 +1,13 @@ +export type SizeType = 'small' | 'middle' | 'large' | undefined + +export function isPresetSize(size?: SizeType | string | number): size is SizeType { + return ['small', 'middle', 'large'].includes(size as string) +} + +export function isValidGapNumber(size?: SizeType | string | number): size is number { + if (!size) { + // The case of size = 0 is deliberately excluded here, because the default value of the gap attribute in CSS is 0, so if the user passes 0 in, we can directly ignore it. + return false + } + return typeof size === 'number' && !Number.isNaN(size) +} diff --git a/packages/ui/src/utils/util.ts b/packages/ui/src/utils/util.ts new file mode 100644 index 000000000..ba253242f --- /dev/null +++ b/packages/ui/src/utils/util.ts @@ -0,0 +1,4 @@ +export const isArray = Array.isArray +export const isString = val => typeof val === 'string' +export const isSymbol = val => typeof val === 'symbol' +export const isObject = val => val !== null && typeof val === 'object'