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 Theme } from './theme'
|
||||
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