refactor: segmented #6286
parent
62e7f94aba
commit
9df8317ece
|
@ -59,8 +59,18 @@ export function functionType<T = () => {}>(defaultVal?: T) {
|
|||
return { type: Function as PropType<T>, default: defaultVal as T };
|
||||
}
|
||||
|
||||
export function anyType<T = any>(defaultVal?: T) {
|
||||
return { validator: () => true, default: defaultVal as T } as unknown as { type: PropType<T> };
|
||||
export function anyType<T = any>(defaultVal?: T, required?: boolean) {
|
||||
const type = { validator: () => true, default: defaultVal as T } as unknown;
|
||||
return required
|
||||
? (type as {
|
||||
type: PropType<T>;
|
||||
default: T;
|
||||
required: true;
|
||||
})
|
||||
: (type as {
|
||||
default: T;
|
||||
type: PropType<T>;
|
||||
});
|
||||
}
|
||||
export function vNodeType<T = VueNode>() {
|
||||
return { validator: () => true } as unknown as { type: PropType<T> };
|
||||
|
|
|
@ -15,17 +15,19 @@ The most basic usage.
|
|||
</docs>
|
||||
|
||||
<template>
|
||||
<a-segmented :options="data" />
|
||||
<a-segmented v-model:value="value" :options="data" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']);
|
||||
const value = ref(data[0]);
|
||||
return {
|
||||
data,
|
||||
value,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,17 +14,19 @@ title:
|
|||
`block` property will make the `Segmented` fit to its parent width.
|
||||
</docs>
|
||||
<template>
|
||||
<a-segmented block :options="data" />
|
||||
<a-segmented v-model:value="value" block :options="data" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data = reactive([123, 456, 'longtext-longtext-longtext-longtext']);
|
||||
const value = ref(data[0]);
|
||||
return {
|
||||
data,
|
||||
value,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<docs>
|
||||
---
|
||||
order: 4
|
||||
title:
|
||||
zh-CN: 受控模式
|
||||
en-US: Controlled mode
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
受控的 Segmented
|
||||
|
||||
## en-US
|
||||
Controlled Segmented.
|
||||
</docs>
|
||||
<template>
|
||||
<div>
|
||||
<a-segmented :options="data" default-value="1" @change="handle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data = reactive(['Map', 'Transit', 'Satellite']);
|
||||
const handle = v => console.log(v);
|
||||
return {
|
||||
data,
|
||||
handle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -14,80 +14,103 @@ title:
|
|||
Custom each Segmented Item.
|
||||
</docs>
|
||||
<template>
|
||||
<a-segmented :options="data">
|
||||
<template #title="index">
|
||||
<template v-if="index === 0">
|
||||
<div style="padding: 4px 4px">
|
||||
<a-avatar src="https://joeschmoe.io/api/v1/random" />
|
||||
<div>User 1</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="index === 1">
|
||||
<div style="padding: 4px 4px">
|
||||
<a-avatar style="background-color: #f56a00">K</a-avatar>
|
||||
<div>User 2</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="index === 2">
|
||||
<div style="padding: 4px 4px">
|
||||
<a-avatar style="background-color: #1890ff">
|
||||
<template #icon><UserOutlined /></template>
|
||||
<a-segmented v-model:value="value" :options="data">
|
||||
<template #label="{ value: val, payload = {} }">
|
||||
<div style="padding: 4px 4px">
|
||||
<template v-if="payload.icon">
|
||||
<a-avatar :src="payload.src" :style="payload.style">
|
||||
<template #icon><component :is="payload.icon" /></template>
|
||||
{{ payload.content }}
|
||||
</a-avatar>
|
||||
<div>User 3</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-avatar :src="payload.src" :style="payload.style">
|
||||
{{ payload.content }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
<div>{{ val }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-segmented>
|
||||
<br />
|
||||
<br />
|
||||
<a-segmented :options="options2">
|
||||
<template #title="index">
|
||||
<template v-if="index === 0">
|
||||
<div style="padding: 4px 4px">
|
||||
<div>Spring</div>
|
||||
<div>Jan-Mar</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="index === 1">
|
||||
<div style="padding: 4px 4px">
|
||||
<div>Summer</div>
|
||||
<div>Apr-Jun</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="index === 2">
|
||||
<div style="padding: 4px 4px">
|
||||
<div>Autumn</div>
|
||||
<div>Jul-Sept</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="index === 3">
|
||||
<div style="padding: 4px 4px">
|
||||
<div>Winter</div>
|
||||
<div>Oct-Dec</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-segmented v-model:value="value2" :options="options2">
|
||||
<template #label="{ payload }">
|
||||
<div style="padding: 4px 4px">
|
||||
<div>{{ payload.title }}</div>
|
||||
<div>{{ payload.subTitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-segmented>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { UserOutlined } from '@ant-design/icons-vue';
|
||||
import ASegmented from 'ant-design-vue/es/segmented/src/segmented';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ASegmented, UserOutlined },
|
||||
setup() {
|
||||
const data = reactive([{ value: 'user1' }, { value: 'user2' }, { value: 'user3' }]);
|
||||
const options2 = reactive([
|
||||
{ value: 'spring' },
|
||||
{ value: 'summer' },
|
||||
{ value: 'autumn' },
|
||||
{ value: 'winter' },
|
||||
const data = ref([
|
||||
{
|
||||
value: 'user1',
|
||||
payload: {
|
||||
src: 'https://joeschmoe.io/api/v1/random',
|
||||
style: { backgroundColor: '#f56a00' },
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'user2',
|
||||
payload: {
|
||||
style: { backgroundColor: '#f56a00' },
|
||||
content: 'K',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'user3',
|
||||
payload: {
|
||||
icon: UserOutlined,
|
||||
style: { backgroundColor: '#f56a00' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
const options2 = ref([
|
||||
{
|
||||
value: 'spring',
|
||||
payload: {
|
||||
title: 'Spring',
|
||||
subTitle: 'Jan-Mar',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'summer',
|
||||
payload: {
|
||||
title: 'Summer',
|
||||
subTitle: 'Apr-Jun',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'autumn',
|
||||
payload: {
|
||||
title: 'Autumn',
|
||||
subTitle: 'Jul-Sept',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'winter',
|
||||
payload: {
|
||||
title: 'Winter',
|
||||
subTitle: 'Oct-Dec',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const value = ref('user1');
|
||||
const value2 = ref('spring');
|
||||
return {
|
||||
data,
|
||||
options2,
|
||||
value,
|
||||
value2,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,15 +15,15 @@ Disabled Segmented.
|
|||
</docs>
|
||||
<template>
|
||||
<div>
|
||||
<a-segmented disabled :options="data" />
|
||||
<a-segmented v-model:value="value" disabled :options="data" />
|
||||
<br />
|
||||
<br />
|
||||
<a-segmented :options="data2" />
|
||||
<a-segmented v-model:value="value2" :options="data2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
|
@ -35,9 +35,13 @@ export default defineComponent({
|
|||
{ value: 'Quarterly', disabled: true },
|
||||
'Yearly',
|
||||
]);
|
||||
const value = ref(data[0]);
|
||||
const value2 = ref('Daily');
|
||||
return {
|
||||
data,
|
||||
data2,
|
||||
value,
|
||||
value2,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,10 +14,10 @@ title:
|
|||
Load dynamically.
|
||||
</docs>
|
||||
<template>
|
||||
<a-segmented :options="data"></a-segmented>
|
||||
<a-segmented v-model:value="value" :options="data"></a-segmented>
|
||||
<br />
|
||||
<br />
|
||||
<a-button type="primary" @click="loadMore" :disabled="isDisabled">Load More</a-button>
|
||||
<a-button type="primary" :disabled="isDisabled" @click="loadMore">Load More</a-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -25,15 +25,17 @@ import { defineComponent, reactive, ref } from 'vue';
|
|||
export default defineComponent({
|
||||
setup() {
|
||||
const data = reactive(['Daily', 'Weekly', 'Monthly']);
|
||||
const isDisabled = ref<boolean>(false);
|
||||
const isDisabled = ref(false);
|
||||
const loadMore = () => {
|
||||
data.push(...['Quarterly', 'Yearly']);
|
||||
isDisabled.value = true;
|
||||
};
|
||||
const value = ref(data[0]);
|
||||
return {
|
||||
data,
|
||||
loadMore,
|
||||
isDisabled,
|
||||
value,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
<docs>
|
||||
---
|
||||
order: 7
|
||||
title:
|
||||
zh-CN: 设置图标
|
||||
en-US: With Icon
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
给 Segmented Item 设置 Icon。
|
||||
|
||||
## en-US
|
||||
Set `icon` for Segmented Item.
|
||||
</docs>
|
||||
<template>
|
||||
<a-segmented :options="data">
|
||||
<template #icon="index">
|
||||
<template v-if="index == 0">
|
||||
<unordered-list-outlined />
|
||||
</template>
|
||||
<template v-if="index == 1">
|
||||
<appstore-outlined />
|
||||
</template>
|
||||
</template>
|
||||
</a-segmented>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { UnorderedListOutlined, AppstoreOutlined },
|
||||
setup() {
|
||||
const data = reactive([
|
||||
{
|
||||
value: 'List',
|
||||
},
|
||||
{
|
||||
value: 'Kanban',
|
||||
},
|
||||
]);
|
||||
return {
|
||||
data,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -3,11 +3,9 @@
|
|||
<basic />
|
||||
<block />
|
||||
<disabled />
|
||||
<controlled />
|
||||
<custom />
|
||||
<dynamic />
|
||||
<size />
|
||||
<icon />
|
||||
</demo-sort>
|
||||
</template>
|
||||
|
||||
|
@ -18,14 +16,12 @@ import US from '../index.en-US.md';
|
|||
import Basic from './basic.vue';
|
||||
import Block from './block.vue';
|
||||
import Disabled from './disabled.vue';
|
||||
import Controlled from './controlled.vue';
|
||||
import Custom from './custom.vue';
|
||||
import Dynamic from './dynamic.vue';
|
||||
import Size from './size.vue';
|
||||
import Icon from './icon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { Icon, Size, Dynamic, Custom, Controlled, Disabled, Block, Basic },
|
||||
components: { Size, Dynamic, Custom, Disabled, Block, Basic },
|
||||
category: 'Components',
|
||||
subtitle: '分段控制器',
|
||||
type: 'Data Display',
|
||||
|
|
|
@ -14,23 +14,29 @@ title:
|
|||
There are three sizes of an a-segmented: `large` (40px), `default` (32px) and `small` (24px).
|
||||
</docs>
|
||||
<template>
|
||||
<a-segmented :options="data" size="large" />
|
||||
<a-segmented v-model:value="value" :options="data" size="large" />
|
||||
<br />
|
||||
<br />
|
||||
<a-segmented :options="data" />
|
||||
<a-segmented v-model:value="value2" :options="data" />
|
||||
<br />
|
||||
<br />
|
||||
<a-segmented :options="data" size="small" />
|
||||
<a-segmented v-model:value="value3" :options="data" size="small" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { defineComponent, ref, reactive } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']);
|
||||
const value = ref(data[0]);
|
||||
const value2 = ref(data[0]);
|
||||
const value3 = ref(data[0]);
|
||||
return {
|
||||
data,
|
||||
value,
|
||||
value2,
|
||||
value3,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,14 +13,35 @@ Segmented Controls.
|
|||
|
||||
## API
|
||||
|
||||
### Segmented
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| block | Option to fit width to its parent\'s width | boolean | false | |
|
||||
| defaultValue | Default selected value | string \| number | | |
|
||||
| disabled | Disable all segments | boolean | false | |
|
||||
| change | The callback function that is triggered when the state changes | function(value: string \| number) | | |
|
||||
| options | Set children optional | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
|
||||
| options | Set children optional | string[] \| number[] \| SegmentedOption[] | [] | |
|
||||
| size | The size of the Segmented. | `large` \| `middle` \| `small` | - | |
|
||||
| value | Currently selected value | string \| number | | |
|
||||
| label | custom label by slot | v-slot:label="SegmentedBaseOption" | | |
|
||||
|
||||
### events
|
||||
|
||||
| Events Name | Description | Arguments | |
|
||||
| --- | --- | --- | --- |
|
||||
| change | The callback function that is triggered when the state changes | function(value: string \| number) | - |
|
||||
|
||||
#### SegmentedBaseOption、 SegmentedOption
|
||||
|
||||
```ts
|
||||
interface SegmentedBaseOption {
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
payload?: any; // payload more data
|
||||
/**
|
||||
* html `title` property for label
|
||||
*/
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
interface SegmentedOption extends SegmentedBaseOption {
|
||||
label?: VueNode | ((option: SegmentedBaseOption) => VueNode);
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import type { App } from 'vue';
|
||||
import Segmented from './src';
|
||||
import type { SegmentedProps } from './src';
|
||||
import { withInstall } from '../_util/type';
|
||||
|
||||
Segmented.install = function (app: App) {
|
||||
app.component(Segmented.name, Segmented);
|
||||
return app;
|
||||
};
|
||||
export default Segmented;
|
||||
export default withInstall(Segmented);
|
||||
export type { SegmentedProps };
|
||||
|
|
|
@ -19,9 +19,32 @@ title: Segmented
|
|||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| block | 将宽度调整为父元素宽度的选项 | boolean | 无 | |
|
||||
| defaultValue | 默认选中的值 | string \| number | | |
|
||||
| disabled | 是否禁用 | boolean | false | |
|
||||
| change | 选项变化时的回调函数 | function(value: string \| number) | | |
|
||||
| options | 数据化配置选项内容 | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
|
||||
| options | 数据化配置选项内容 | string[] \| number[] \| SegmentedOption[] | [] | |
|
||||
| size | 控件尺寸 | `large` \| `middle` \| `small` | - | |
|
||||
| value | 当前选中的值 | string \| number | | |
|
||||
| label | 使用插槽自定义 label | v-slot:label="SegmentedBaseOption" | | |
|
||||
|
||||
### 事件
|
||||
|
||||
| 事件名称 | 说明 | 回调参数 | |
|
||||
| -------- | -------------------- | --------------------------------- | --- |
|
||||
| change | 选项变化时的回调函数 | function(value: string \| number) | - |
|
||||
|
||||
#### SegmentedBaseOption、SegmentedOption
|
||||
|
||||
```ts
|
||||
interface SegmentedBaseOption {
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
payload?: any; // payload more data
|
||||
/**
|
||||
* html `title` property for label
|
||||
*/
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
interface SegmentedOption extends SegmentedBaseOption {
|
||||
label?: VueNode | ((option: SegmentedBaseOption) => VueNode);
|
||||
}
|
||||
```
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
import { addClass, removeClass } from 'ant-design-vue/es/vc-util/Dom/class';
|
||||
import type { CSSProperties, Ref, TransitionProps } from 'vue';
|
||||
import { onBeforeUnmount, nextTick, Transition, watch, defineComponent, computed, ref } from 'vue';
|
||||
import { anyType } from '../../_util/type';
|
||||
import type { SegmentedValue } from './segmented';
|
||||
|
||||
type ThumbReact = {
|
||||
left: number;
|
||||
right: number;
|
||||
width: number;
|
||||
} | null;
|
||||
|
||||
export interface MotionThumbInterface {
|
||||
value: SegmentedValue;
|
||||
getValueIndex: (value: SegmentedValue) => number;
|
||||
prefixCls: string;
|
||||
motionName: string;
|
||||
onMotionStart: VoidFunction;
|
||||
onMotionEnd: VoidFunction;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
}
|
||||
|
||||
const calcThumbStyle = (targetElement: HTMLElement | null | undefined): ThumbReact =>
|
||||
targetElement
|
||||
? {
|
||||
left: targetElement.offsetLeft,
|
||||
right:
|
||||
(targetElement.parentElement!.clientWidth as number) -
|
||||
targetElement.clientWidth -
|
||||
targetElement.offsetLeft,
|
||||
width: targetElement.clientWidth,
|
||||
}
|
||||
: null;
|
||||
|
||||
const toPX = (value?: number) => (value !== undefined ? `${value}px` : undefined);
|
||||
|
||||
const MotionThumb = defineComponent({
|
||||
props: {
|
||||
value: anyType<SegmentedValue>(),
|
||||
getValueIndex: anyType<(value: SegmentedValue) => number>(),
|
||||
prefixCls: anyType<string>(),
|
||||
motionName: anyType<string>(),
|
||||
onMotionStart: anyType<VoidFunction>(),
|
||||
onMotionEnd: anyType<VoidFunction>(),
|
||||
direction: anyType<'ltr' | 'rtl'>(),
|
||||
containerRef: anyType<Ref<HTMLDivElement>>(),
|
||||
},
|
||||
emits: ['motionStart', 'motionEnd'],
|
||||
setup(props, { emit }) {
|
||||
const thumbRef = ref<HTMLDivElement>();
|
||||
// =========================== Effect ===========================
|
||||
const findValueElement = (val: SegmentedValue) => {
|
||||
const index = props.getValueIndex(val);
|
||||
|
||||
const ele = props.containerRef.value?.querySelectorAll<HTMLDivElement>(
|
||||
`.${props.prefixCls}-item`,
|
||||
)[index];
|
||||
return ele?.offsetParent && ele;
|
||||
};
|
||||
|
||||
const prevStyle = ref<ThumbReact>(null);
|
||||
const nextStyle = ref<ThumbReact>(null);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value, prevValue) => {
|
||||
const prev = findValueElement(prevValue);
|
||||
const next = findValueElement(value);
|
||||
|
||||
const calcPrevStyle = calcThumbStyle(prev);
|
||||
const calcNextStyle = calcThumbStyle(next);
|
||||
|
||||
prevStyle.value = calcPrevStyle;
|
||||
nextStyle.value = calcNextStyle;
|
||||
|
||||
if (prev && next) {
|
||||
emit('motionStart');
|
||||
} else {
|
||||
emit('motionEnd');
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
const thumbStart = computed(() =>
|
||||
props.direction === 'rtl'
|
||||
? toPX(-(prevStyle.value?.right as number))
|
||||
: toPX(prevStyle.value?.left as number),
|
||||
);
|
||||
const thumbActive = computed(() =>
|
||||
props.direction === 'rtl'
|
||||
? toPX(-(nextStyle.value?.right as number))
|
||||
: toPX(nextStyle.value?.left as number),
|
||||
);
|
||||
|
||||
// =========================== Motion ===========================
|
||||
let timeid: any;
|
||||
const onAppearStart: TransitionProps['onBeforeEnter'] = (el: HTMLDivElement) => {
|
||||
clearTimeout(timeid);
|
||||
nextTick(() => {
|
||||
if (el) {
|
||||
el.style.transform = `translateX(var(--thumb-start-left))`;
|
||||
el.style.width = `var(--thumb-start-width)`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onAppearActive: TransitionProps['onEnter'] = (el: HTMLDivElement) => {
|
||||
timeid = setTimeout(() => {
|
||||
if (el) {
|
||||
addClass(el, `${props.motionName}-appear-active`);
|
||||
el.style.transform = `translateX(var(--thumb-active-left))`;
|
||||
el.style.width = `var(--thumb-active-width)`;
|
||||
}
|
||||
});
|
||||
};
|
||||
const onAppearEnd: TransitionProps['onAfterEnter'] = (el: HTMLDivElement) => {
|
||||
prevStyle.value = null;
|
||||
nextStyle.value = null;
|
||||
if (el) {
|
||||
el.style.transform = null;
|
||||
el.style.width = null;
|
||||
removeClass(el, `${props.motionName}-appear-active`);
|
||||
}
|
||||
emit('motionEnd');
|
||||
};
|
||||
const mergedStyle = computed<CSSProperties>(() => ({
|
||||
'--thumb-start-left': thumbStart.value,
|
||||
'--thumb-start-width': toPX(prevStyle.value?.width),
|
||||
'--thumb-active-left': thumbActive.value,
|
||||
'--thumb-active-width': toPX(nextStyle.value?.width),
|
||||
}));
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(timeid);
|
||||
});
|
||||
return () => {
|
||||
// It's little ugly which should be refactor when @umi/test update to latest jsdom
|
||||
const motionProps = {
|
||||
ref: thumbRef,
|
||||
style: mergedStyle.value,
|
||||
class: [`${props.prefixCls}-thumb`],
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
(motionProps as any)['data-test-style'] = JSON.stringify(mergedStyle.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
onBeforeEnter={onAppearStart}
|
||||
onEnter={onAppearActive}
|
||||
onAfterEnter={onAppearEnd}
|
||||
>
|
||||
{!prevStyle.value || !nextStyle.value ? null : <div {...motionProps}></div>}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
export default MotionThumb;
|
|
@ -1,151 +1,170 @@
|
|||
import { defineComponent, ref, toRefs, reactive, watch } from 'vue';
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import type { ExtractPropTypes, FunctionalComponent } from 'vue';
|
||||
import classNames from '../../_util/classNames';
|
||||
import useConfigInject from '../../config-provider/hooks/useConfigInject';
|
||||
import { getPropsSlot, initDefaultProps } from '../../_util/props-util';
|
||||
import { initDefaultProps } from '../../_util/props-util';
|
||||
import useStyle from '../style';
|
||||
|
||||
import type { VueNode } from '../../_util/type';
|
||||
import { someType, arrayType, booleanType, stringType } from '../../_util/type';
|
||||
import type { ChangeEvent } from '../../_util/EventInterface';
|
||||
import MotionThumb from './MotionThumb';
|
||||
export type SegmentedValue = string | number;
|
||||
export type segmentedSize = 'large' | 'small';
|
||||
export interface SegmentedOptions {
|
||||
value?: string;
|
||||
export interface SegmentedBaseOption {
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
payload?: any;
|
||||
/**
|
||||
* html `title` property for label
|
||||
*/
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
export interface SegmentedOption extends SegmentedBaseOption {
|
||||
label?: VueNode | ((option: SegmentedBaseOption) => VueNode);
|
||||
}
|
||||
|
||||
function normalizeOptions(options: (SegmentedOption | string | number)[]) {
|
||||
return options.map(option => {
|
||||
if (typeof option === 'object' && option !== null) {
|
||||
return option;
|
||||
}
|
||||
|
||||
return {
|
||||
label: option?.toString(),
|
||||
title: option?.toString(),
|
||||
value: option as unknown as SegmentedBaseOption['value'],
|
||||
};
|
||||
});
|
||||
}
|
||||
export const segmentedProps = () => {
|
||||
return {
|
||||
options: { type: Array as PropType<Array<SegmentedOptions | string | number>> },
|
||||
defaultValue: { type: [Number, String] },
|
||||
block: Boolean,
|
||||
disabled: Boolean,
|
||||
size: { type: String as PropType<segmentedSize> },
|
||||
prefixCls: String,
|
||||
options: arrayType<(SegmentedOption | string | number)[]>(),
|
||||
block: booleanType(),
|
||||
disabled: booleanType(),
|
||||
size: stringType<segmentedSize>(),
|
||||
value: { ...someType<SegmentedValue>([String, Number]), required: true },
|
||||
motionName: String,
|
||||
};
|
||||
};
|
||||
export type SegmentedProps = Partial<ExtractPropTypes<ReturnType<typeof segmentedProps>>>;
|
||||
|
||||
const SegmentedOption: FunctionalComponent<
|
||||
SegmentedOption & { prefixCls: string; checked: boolean }
|
||||
> = (props, { slots, emit, attrs }) => {
|
||||
const { value, disabled, payload, title, prefixCls, label = slots.label, checked } = props;
|
||||
const handleChange = (event: InputEvent) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('change', event, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
class={classNames(
|
||||
{
|
||||
[`${prefixCls}-item-disabled`]: disabled,
|
||||
},
|
||||
attrs.class,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
class={`${prefixCls}-item-input`}
|
||||
type="radio"
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div class={`${prefixCls}-item-label`} title={typeof title === 'string' ? title : ''}>
|
||||
{typeof label === 'function'
|
||||
? label({
|
||||
value,
|
||||
disabled,
|
||||
payload,
|
||||
title,
|
||||
})
|
||||
: label}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
SegmentedOption.inheritAttrs = false;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ASegmented',
|
||||
inheritAttrs: false,
|
||||
props: { ...initDefaultProps(segmentedProps(), {}) },
|
||||
emits: ['change', 'value'],
|
||||
slots: ['icon', 'title'],
|
||||
setup(props, { emit, slots }) {
|
||||
const { prefixCls } = useConfigInject('segmented', props);
|
||||
props: initDefaultProps(segmentedProps(), {
|
||||
options: [],
|
||||
motionName: 'thumb-motion',
|
||||
}),
|
||||
emits: ['change', 'update:value'],
|
||||
slots: ['label'],
|
||||
setup(props, { emit, slots, attrs }) {
|
||||
const { prefixCls, direction, size } = useConfigInject('segmented', props);
|
||||
const [wrapSSR, hashId] = useStyle(prefixCls);
|
||||
const pre = prefixCls.value;
|
||||
const { size } = toRefs(props);
|
||||
const itemRef = ref([]);
|
||||
const { options, disabled, defaultValue } = toRefs(props);
|
||||
const segmentedItemInput = () => {
|
||||
return <input type="radio" class={`${pre}-item-input`} disabled checked />;
|
||||
};
|
||||
const isDisabled = item => {
|
||||
if (disabled.value || (typeof item == 'object' && item.disabled)) {
|
||||
return segmentedItemInput();
|
||||
const rootRef = ref<HTMLDivElement>();
|
||||
const thumbShow = ref(false);
|
||||
|
||||
const segmentedOptions = computed(() => normalizeOptions(props.options));
|
||||
const handleChange = (_event: ChangeEvent, val: SegmentedValue) => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
const currentItemKey = ref();
|
||||
currentItemKey.value = defaultValue.value ? defaultValue.value : 0;
|
||||
const toPX = (value: number) => (value !== undefined ? `${value}px` : undefined);
|
||||
// 开始 or 停止
|
||||
const thumbShow = ref(true);
|
||||
const mergedStyle = reactive({
|
||||
startLeft: '',
|
||||
startWidth: '',
|
||||
activeLeft: '',
|
||||
activeWidth: '',
|
||||
});
|
||||
const handleSelectedChange = (item, index) => {
|
||||
if (disabled.value || item.disabled) return;
|
||||
currentItemKey.value = index;
|
||||
emit('change', { value: item, key: index });
|
||||
};
|
||||
const icon = getPropsSlot(slots, props, 'icon');
|
||||
const title = getPropsSlot(slots, props, 'title');
|
||||
const iconNode = index => {
|
||||
return icon ? (
|
||||
<span class={classNames({ [`${pre}-item-icon`]: icon })}>{slots.icon?.(index)}</span>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
};
|
||||
const itemNode = (item, index) => {
|
||||
if (title) {
|
||||
return <div>{slots.title?.(index)}</div>;
|
||||
}
|
||||
return <span>{item.value}</span>;
|
||||
};
|
||||
const calcThumbStyle = index => {
|
||||
return {
|
||||
left: itemRef.value[index].children[0].offsetParent.offsetLeft,
|
||||
width: itemRef.value[index].children[0].clientWidth,
|
||||
};
|
||||
};
|
||||
const thumbStyle = reactive({
|
||||
transform: '',
|
||||
width: '',
|
||||
});
|
||||
const isValueType = item => {
|
||||
return item instanceof Object ? (item.disabled ? true : false) : false;
|
||||
};
|
||||
watch(
|
||||
() => currentItemKey.value,
|
||||
(newValue, oldValue) => {
|
||||
const prev = oldValue ? oldValue : defaultValue.value ? defaultValue.value : 0;
|
||||
const next = newValue;
|
||||
const calcPrevStyle = calcThumbStyle(prev);
|
||||
const calcNextStyle = calcThumbStyle(next);
|
||||
mergedStyle.startLeft = toPX(calcPrevStyle.left);
|
||||
mergedStyle.startWidth = toPX(calcPrevStyle.width);
|
||||
mergedStyle.activeLeft = toPX(calcNextStyle.left);
|
||||
mergedStyle.activeWidth = toPX(calcNextStyle.width);
|
||||
if (prev !== next) {
|
||||
thumbStyle.transform = `translateX(${mergedStyle.activeLeft})`;
|
||||
thumbStyle.width = `${mergedStyle.activeWidth}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
const thumbNode = () => {
|
||||
return thumbShow.value ? (
|
||||
<div
|
||||
class={classNames({
|
||||
[`${pre}-thumb`]: thumbShow.value,
|
||||
[`${pre}-thumb-motion-appear-active`]: thumbShow.value,
|
||||
})}
|
||||
style={thumbStyle}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
emit('update:value', val);
|
||||
emit('change', val);
|
||||
};
|
||||
return () => {
|
||||
const pre = prefixCls.value;
|
||||
return wrapSSR(
|
||||
<div
|
||||
class={classNames(pre, {
|
||||
[hashId.value]: true,
|
||||
[`${pre}-block`]: props.block,
|
||||
[`${pre}-item-disabled`]: props.disabled,
|
||||
[`${pre}-lg`]: size.value == 'large',
|
||||
[`${pre}-sm`]: size.value == 'small',
|
||||
})}
|
||||
{...attrs}
|
||||
class={classNames(
|
||||
pre,
|
||||
{
|
||||
[hashId.value]: true,
|
||||
[`${pre}-block`]: props.block,
|
||||
[`${pre}-disabled`]: props.disabled,
|
||||
[`${pre}-lg`]: size.value == 'large',
|
||||
[`${pre}-sm`]: size.value == 'small',
|
||||
[`${pre}-rtl`]: direction.value === 'rtl',
|
||||
},
|
||||
attrs.class,
|
||||
)}
|
||||
ref={rootRef}
|
||||
>
|
||||
<div class={classNames(`${pre}-group`)}>
|
||||
{thumbNode()}
|
||||
{options.value.map((item, index) => {
|
||||
return (
|
||||
<label
|
||||
ref={ref => (itemRef.value[index] = ref)}
|
||||
class={classNames(`${pre}-item`, {
|
||||
[`${pre}-item-selected`]: currentItemKey.value == index,
|
||||
[`${pre}-item-disabled`]: disabled.value || isValueType(item),
|
||||
})}
|
||||
onClick={() => handleSelectedChange(item, index)}
|
||||
>
|
||||
{isDisabled(item)}
|
||||
<div class={classNames(`${pre}-item-label`)} key={index}>
|
||||
{iconNode(index)}
|
||||
{typeof item == 'object' ? itemNode(item, index) : item}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<div class={`${pre}-group`}>
|
||||
<MotionThumb
|
||||
containerRef={rootRef}
|
||||
prefixCls={pre}
|
||||
value={props.value}
|
||||
motionName={`${pre}-${props.motionName}`}
|
||||
direction={direction.value}
|
||||
getValueIndex={val => segmentedOptions.value.findIndex(n => n.value === val)}
|
||||
onMotionStart={() => {
|
||||
thumbShow.value = true;
|
||||
}}
|
||||
onMotionEnd={() => {
|
||||
thumbShow.value = false;
|
||||
}}
|
||||
/>
|
||||
{segmentedOptions.value.map(segmentedOption => (
|
||||
<SegmentedOption
|
||||
key={segmentedOption.value}
|
||||
prefixCls={pre}
|
||||
class={classNames(segmentedOption.className, `${pre}-item`, {
|
||||
[`${pre}-item-selected`]:
|
||||
segmentedOption.value === props.value && !thumbShow.value,
|
||||
})}
|
||||
checked={segmentedOption.value === props.value}
|
||||
onChange={handleChange}
|
||||
{...segmentedOption}
|
||||
disabled={!!props.disabled || !!segmentedOption.disabled}
|
||||
v-slots={slots}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ interface SegmentedToken extends FullToken<'Segmented'> {
|
|||
}
|
||||
|
||||
// ============================== Mixins ==============================
|
||||
function segmentedDisabledItem(cls: string, token: SegmentedToken): CSSObject {
|
||||
function getItemDisabledStyle(cls: string, token: SegmentedToken): CSSObject {
|
||||
return {
|
||||
[`${cls}, ${cls}:hover, ${cls}:focus`]: {
|
||||
color: token.colorTextDisabled,
|
||||
|
@ -26,7 +26,7 @@ function segmentedDisabledItem(cls: string, token: SegmentedToken): CSSObject {
|
|||
};
|
||||
}
|
||||
|
||||
function getSegmentedItemSelectedStyle(token: SegmentedToken): CSSObject {
|
||||
function getItemSelectedStyle(token: SegmentedToken): CSSObject {
|
||||
return {
|
||||
backgroundColor: token.bgColorSelected,
|
||||
boxShadow: token.boxShadow,
|
||||
|
@ -39,8 +39,8 @@ const segmentedTextEllipsisCss: CSSObject = {
|
|||
...textEllipsis,
|
||||
};
|
||||
|
||||
// ============================== Shared ==============================
|
||||
const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObject => {
|
||||
// ============================== Styles ==============================
|
||||
const genSegmentedStyle: GenerateStyle<SegmentedToken> = (token: SegmentedToken) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
|
@ -63,16 +63,16 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
},
|
||||
|
||||
// RTL styles
|
||||
'&&-rtl': {
|
||||
[`&${componentCls}-rtl`]: {
|
||||
direction: 'rtl',
|
||||
},
|
||||
|
||||
// block styles
|
||||
'&&-block': {
|
||||
[`&${componentCls}-block`]: {
|
||||
display: 'flex',
|
||||
},
|
||||
|
||||
[`&&-block ${componentCls}-item`]: {
|
||||
[`&${componentCls}-block ${componentCls}-item`]: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
|
@ -86,7 +86,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
borderRadius: token.borderRadiusSM,
|
||||
|
||||
'&-selected': {
|
||||
...getSegmentedItemSelectedStyle(token),
|
||||
...getItemSelectedStyle(token),
|
||||
color: token.labelColorHover,
|
||||
},
|
||||
|
||||
|
@ -97,7 +97,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
height: '100%',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
borderRadius: 'inherit',
|
||||
transition: `background-color ${token.motionDurationMid}`,
|
||||
},
|
||||
|
||||
|
@ -118,7 +118,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
|
||||
// syntactic sugar to add `icon` for Segmented Item
|
||||
'&-icon + *': {
|
||||
marginInlineEnd: token.marginSM / 2,
|
||||
marginInlineStart: token.marginSM / 2,
|
||||
},
|
||||
|
||||
'&-input': {
|
||||
|
@ -132,39 +132,9 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
},
|
||||
},
|
||||
|
||||
// size styles
|
||||
'&&-lg': {
|
||||
borderRadius: token.borderRadiusLG,
|
||||
[`${componentCls}-item-label`]: {
|
||||
minHeight: token.controlHeightLG - token.segmentedContainerPadding * 2,
|
||||
lineHeight: `${token.controlHeightLG - token.segmentedContainerPadding * 2}px`,
|
||||
padding: `0 ${token.segmentedPaddingHorizontal}px`,
|
||||
fontSize: token.fontSizeLG,
|
||||
},
|
||||
[`${componentCls}-item-selected`]: {
|
||||
borderRadius: token.borderRadius,
|
||||
},
|
||||
},
|
||||
|
||||
'&&-sm': {
|
||||
borderRadius: token.borderRadiusSM,
|
||||
[`${componentCls}-item-label`]: {
|
||||
minHeight: token.controlHeightSM - token.segmentedContainerPadding * 2,
|
||||
lineHeight: `${token.controlHeightSM - token.segmentedContainerPadding * 2}px`,
|
||||
padding: `0 ${token.segmentedPaddingHorizontalSM}px`,
|
||||
},
|
||||
[`${componentCls}-item-selected`]: {
|
||||
borderRadius: token.borderRadiusXS,
|
||||
},
|
||||
},
|
||||
|
||||
// disabled styles
|
||||
...segmentedDisabledItem(`&-disabled ${componentCls}-item`, token),
|
||||
...segmentedDisabledItem(`${componentCls}-item-disabled`, token),
|
||||
|
||||
// thumb styles
|
||||
[`${componentCls}-thumb`]: {
|
||||
...getSegmentedItemSelectedStyle(token),
|
||||
...getItemSelectedStyle(token),
|
||||
|
||||
position: 'absolute',
|
||||
insetBlockStart: 0,
|
||||
|
@ -180,6 +150,36 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
},
|
||||
},
|
||||
|
||||
// size styles
|
||||
[`&${componentCls}-lg`]: {
|
||||
borderRadius: token.borderRadiusLG,
|
||||
[`${componentCls}-item-label`]: {
|
||||
minHeight: token.controlHeightLG - token.segmentedContainerPadding * 2,
|
||||
lineHeight: `${token.controlHeightLG - token.segmentedContainerPadding * 2}px`,
|
||||
padding: `0 ${token.segmentedPaddingHorizontal}px`,
|
||||
fontSize: token.fontSizeLG,
|
||||
},
|
||||
[`${componentCls}-item, ${componentCls}-thumb`]: {
|
||||
borderRadius: token.borderRadius,
|
||||
},
|
||||
},
|
||||
|
||||
[`&${componentCls}-sm`]: {
|
||||
borderRadius: token.borderRadiusSM,
|
||||
[`${componentCls}-item-label`]: {
|
||||
minHeight: token.controlHeightSM - token.segmentedContainerPadding * 2,
|
||||
lineHeight: `${token.controlHeightSM - token.segmentedContainerPadding * 2}px`,
|
||||
padding: `0 ${token.segmentedPaddingHorizontalSM}px`,
|
||||
},
|
||||
[`${componentCls}-item, ${componentCls}-thumb`]: {
|
||||
borderRadius: token.borderRadiusXS,
|
||||
},
|
||||
},
|
||||
|
||||
// disabled styles
|
||||
...getItemDisabledStyle(`&-disabled ${componentCls}-item`, token),
|
||||
...getItemDisabledStyle(`${componentCls}-item-disabled`, token),
|
||||
|
||||
// transition effect when `appear-active`
|
||||
[`${componentCls}-thumb-motion-appear-active`]: {
|
||||
transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOut}, width ${token.motionDurationSlow} ${token.motionEaseInOut}`,
|
||||
|
@ -188,6 +188,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default genComponentStyleHook('Segmented', token => {
|
||||
const {
|
||||
|
@ -210,5 +211,5 @@ export default genComponentStyleHook('Segmented', token => {
|
|||
bgColorHover: colorFillSecondary,
|
||||
bgColorSelected: colorBgElevated,
|
||||
});
|
||||
return [genSharedSegmentedStyle(segmentedToken)];
|
||||
return [genSegmentedStyle(segmentedToken)];
|
||||
});
|
||||
|
|
|
@ -163,6 +163,8 @@ declare module 'vue' {
|
|||
|
||||
ASelect: typeof import('ant-design-vue')['Select'];
|
||||
|
||||
ASegmented: typeof import('ant-design-vue')['Segmented'];
|
||||
|
||||
ASelectOptGroup: typeof import('ant-design-vue')['SelectOptGroup'];
|
||||
|
||||
ASelectOption: typeof import('ant-design-vue')['SelectOption'];
|
||||
|
|
Loading…
Reference in New Issue