parent
5cc4a63480
commit
a15bb9cfb8
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<a-flex gap="middle" vertical>
|
||||||
|
<label>
|
||||||
|
Select axis:
|
||||||
|
<select v-model="axis">
|
||||||
|
<option v-for="item in axisOptions" :key="item">{{ item }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<a-flex :vertical="axis === 'vertical'">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in new Array(4)"
|
||||||
|
:key="item"
|
||||||
|
:style="{ ...baseStyle, background: `${index % 2 ? '#1677ff' : '#1677ffbf'}` }"
|
||||||
|
/>
|
||||||
|
</a-flex>
|
||||||
|
<hr/>
|
||||||
|
<label>
|
||||||
|
Select justify:
|
||||||
|
<select v-model="justify">
|
||||||
|
<option v-for="item in justifyOptions" :key="item">{{ item }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Select align:
|
||||||
|
<select v-model="align">
|
||||||
|
<option v-for="item in alignOptions" :key="item">{{ item }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<a-flex :style="{ ...boxStyle }" :justify="justify" :align="align">
|
||||||
|
<a-button variant="solid">Primary</a-button>
|
||||||
|
<a-button variant="solid">Primary</a-button>
|
||||||
|
<a-button variant="solid">Primary</a-button>
|
||||||
|
<a-button variant="solid">Primary</a-button>
|
||||||
|
</a-flex>
|
||||||
|
<hr/>
|
||||||
|
<a-flex gap="middle" vertical>
|
||||||
|
<label>
|
||||||
|
Select gap size:
|
||||||
|
<select v-model="gapSize">
|
||||||
|
<option v-for="item in gapSizeOptions" :key="item">{{ item }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<a-flex :gap="gapSize">
|
||||||
|
<a-button variant="solid">Primary</a-button>
|
||||||
|
<a-button>Default</a-button>
|
||||||
|
<a-button variant="dashed">Dashed</a-button>
|
||||||
|
<a-button variant="link">Link</a-button>
|
||||||
|
</a-flex>
|
||||||
|
</a-flex>
|
||||||
|
<hr/>
|
||||||
|
<label>
|
||||||
|
Auto wrap:
|
||||||
|
</label>
|
||||||
|
<a-flex wrap="wrap" gap="small">
|
||||||
|
<a-button v-for="item in new Array(24)" :key="item" variant="solid">Button</a-button>
|
||||||
|
</a-flex>
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
|
||||||
|
const baseStyle: CSSProperties = {
|
||||||
|
width: '25%',
|
||||||
|
height: '54px',
|
||||||
|
};
|
||||||
|
const boxStyle: CSSProperties = {
|
||||||
|
width: '100%',
|
||||||
|
height: '120px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #40a9ff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const axisOptions = reactive(['horizontal', 'vertical']);
|
||||||
|
const axis = ref(axisOptions[0]);
|
||||||
|
|
||||||
|
const justifyOptions = reactive([
|
||||||
|
'flex-start',
|
||||||
|
'center',
|
||||||
|
'flex-end',
|
||||||
|
'space-between',
|
||||||
|
'space-around',
|
||||||
|
'space-evenly',
|
||||||
|
]);
|
||||||
|
const justify = ref(justifyOptions[0]);
|
||||||
|
|
||||||
|
const alignOptions = reactive(['flex-start', 'center', 'flex-end']);
|
||||||
|
const align = ref(alignOptions[0]);
|
||||||
|
|
||||||
|
const gapSizeOptions = reactive(['small', 'middle', 'large']);
|
||||||
|
const gapSize = ref(gapSizeOptions[0]);
|
||||||
|
</script>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue'
|
||||||
|
import { computed, withDefaults } from 'vue'
|
||||||
|
import { type FlexProps, flexDefaultProps } from './meta'
|
||||||
|
import { isPresetSize } from '@/utils/gapSize'
|
||||||
|
import createFlexClassNames from './utils'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AFlex' })
|
||||||
|
const props = withDefaults(defineProps<FlexProps>(), flexDefaultProps)
|
||||||
|
|
||||||
|
const mergedCls = computed(() => [
|
||||||
|
createFlexClassNames(props.prefixCls, props),
|
||||||
|
{
|
||||||
|
'ant-flex': true,
|
||||||
|
'ant-flex-vertical': props.vertical,
|
||||||
|
'ant-flex-rtl': false,
|
||||||
|
[`ant-flex-gap-${props.gap}`]: isPresetSize(props.gap),
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const mergedStyle = computed(() => {
|
||||||
|
const style: CSSProperties = {}
|
||||||
|
|
||||||
|
if (props.flex) {
|
||||||
|
style.flex = props.flex
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.gap && !isPresetSize(props.gap)) {
|
||||||
|
style.gap = `${props.gap}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="componentTag" :class="[$attrs.class, mergedCls]" :style="[$attrs.style, mergedStyle]" v-bind="$attrs">
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Flex > should render correctly 1`] = `
|
||||||
|
"<div class="ant-flex">
|
||||||
|
<div>test</div>
|
||||||
|
</div>"
|
||||||
|
`;
|
|
@ -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: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Flex', () => {
|
||||||
|
const wrapper = mount(Flex, {
|
||||||
|
props: {
|
||||||
|
justify: 'center'
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: `<div>test</div>`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper3 = mount(Flex, {
|
||||||
|
props: {
|
||||||
|
flex: '0 1 auto',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes('ant-flex')).toBeTruthy();
|
||||||
|
expect(wrapper.classes('ant-flex-gap-small')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Component work', () => {
|
||||||
|
const wrapper = mount(Flex, {
|
||||||
|
slots: {
|
||||||
|
default: `<div>test</div>`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper2 = mount(Flex, {
|
||||||
|
props: {
|
||||||
|
componentTag: 'span'
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: `<div>test</div>`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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: `<div>test</div>`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper2 = mount(Flex, {
|
||||||
|
props: {
|
||||||
|
vertical: true,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper2 = mount(Flex, {
|
||||||
|
props: {
|
||||||
|
wrap: true,
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: `<div>test</div>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes('ant-flex-wrap-wrap')).toBeTruthy();
|
||||||
|
expect(wrapper2.classes('ant-flex-wrap-wrap')).toBeTruthy();
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<PropertyKey, boolean> = {}
|
||||||
|
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<PropertyKey, boolean> = {}
|
||||||
|
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<PropertyKey, boolean> = {}
|
||||||
|
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
|
|
@ -2,3 +2,4 @@ export { default as Button } from './button'
|
||||||
export { default as Input } from './input'
|
export { default as Input } from './input'
|
||||||
export { default as Theme } from './theme'
|
export { default as Theme } from './theme'
|
||||||
export { default as Affix } from './affix'
|
export { default as Affix } from './affix'
|
||||||
|
export { default as Flex } from './flex'
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -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'
|
Loading…
Reference in New Issue