refactor: segmented #6286

pull/6296/head^2
tangjinzhou 2 years ago
parent 62e7f94aba
commit 9df8317ece

@ -59,8 +59,18 @@ export function functionType<T = () => {}>(defaultVal?: T) {
return { type: Function as PropType<T>, default: defaultVal as T }; return { type: Function as PropType<T>, default: defaultVal as T };
} }
export function anyType<T = any>(defaultVal?: T) { export function anyType<T = any>(defaultVal?: T, required?: boolean) {
return { validator: () => true, default: defaultVal as T } as unknown as { type: PropType<T> }; 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>() { export function vNodeType<T = VueNode>() {
return { validator: () => true } as unknown as { type: PropType<T> }; return { validator: () => true } as unknown as { type: PropType<T> };

@ -15,17 +15,19 @@ The most basic usage.
</docs> </docs>
<template> <template>
<a-segmented :options="data" /> <a-segmented v-model:value="value" :options="data" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive } from 'vue'; import { defineComponent, reactive, ref } from 'vue';
export default defineComponent({ export default defineComponent({
setup() { setup() {
const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']); const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']);
const value = ref(data[0]);
return { return {
data, data,
value,
}; };
}, },
}); });

@ -14,17 +14,19 @@ title:
`block` property will make the `Segmented` fit to its parent width. `block` property will make the `Segmented` fit to its parent width.
</docs> </docs>
<template> <template>
<a-segmented block :options="data" /> <a-segmented v-model:value="value" block :options="data" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive } from 'vue'; import { defineComponent, reactive, ref } from 'vue';
export default defineComponent({ export default defineComponent({
setup() { setup() {
const data = reactive([123, 456, 'longtext-longtext-longtext-longtext']); const data = reactive([123, 456, 'longtext-longtext-longtext-longtext']);
const value = ref(data[0]);
return { return {
data, 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. Custom each Segmented Item.
</docs> </docs>
<template> <template>
<a-segmented :options="data"> <a-segmented v-model:value="value" :options="data">
<template #title="index"> <template #label="{ value: val, payload = {} }">
<template v-if="index === 0"> <div style="padding: 4px 4px">
<div style="padding: 4px 4px"> <template v-if="payload.icon">
<a-avatar src="https://joeschmoe.io/api/v1/random" /> <a-avatar :src="payload.src" :style="payload.style">
<div>User 1</div> <template #icon><component :is="payload.icon" /></template>
</div> {{ payload.content }}
</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-avatar> </a-avatar>
<div>User 3</div> </template>
</div> <template v-else>
</template> <a-avatar :src="payload.src" :style="payload.style">
{{ payload.content }}
</a-avatar>
</template>
<div>{{ val }}</div>
</div>
</template> </template>
</a-segmented> </a-segmented>
<br /> <br />
<br /> <br />
<a-segmented :options="options2"> <a-segmented v-model:value="value2" :options="options2">
<template #title="index"> <template #label="{ payload }">
<template v-if="index === 0"> <div style="padding: 4px 4px">
<div style="padding: 4px 4px"> <div>{{ payload.title }}</div>
<div>Spring</div> <div>{{ payload.subTitle }}</div>
<div>Jan-Mar</div> </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>
</template> </template>
</a-segmented> </a-segmented>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive } from 'vue'; import { defineComponent, ref } from 'vue';
import { UserOutlined } from '@ant-design/icons-vue'; import { UserOutlined } from '@ant-design/icons-vue';
import ASegmented from 'ant-design-vue/es/segmented/src/segmented'; import ASegmented from 'ant-design-vue/es/segmented/src/segmented';
export default defineComponent({ export default defineComponent({
components: { ASegmented, UserOutlined }, components: { ASegmented, UserOutlined },
setup() { setup() {
const data = reactive([{ value: 'user1' }, { value: 'user2' }, { value: 'user3' }]); const data = ref([
const options2 = reactive([ {
{ value: 'spring' }, value: 'user1',
{ value: 'summer' }, payload: {
{ value: 'autumn' }, src: 'https://joeschmoe.io/api/v1/random',
{ value: 'winter' }, 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 { return {
data, data,
options2, options2,
value,
value2,
}; };
}, },
}); });

@ -15,15 +15,15 @@ Disabled Segmented.
</docs> </docs>
<template> <template>
<div> <div>
<a-segmented disabled :options="data" /> <a-segmented v-model:value="value" disabled :options="data" />
<br /> <br />
<br /> <br />
<a-segmented :options="data2" /> <a-segmented v-model:value="value2" :options="data2" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive } from 'vue'; import { defineComponent, reactive, ref } from 'vue';
export default defineComponent({ export default defineComponent({
setup() { setup() {
@ -35,9 +35,13 @@ export default defineComponent({
{ value: 'Quarterly', disabled: true }, { value: 'Quarterly', disabled: true },
'Yearly', 'Yearly',
]); ]);
const value = ref(data[0]);
const value2 = ref('Daily');
return { return {
data, data,
data2, data2,
value,
value2,
}; };
}, },
}); });

@ -14,10 +14,10 @@ title:
Load dynamically. Load dynamically.
</docs> </docs>
<template> <template>
<a-segmented :options="data"></a-segmented> <a-segmented v-model:value="value" :options="data"></a-segmented>
<br /> <br />
<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> </template>
<script lang="ts"> <script lang="ts">
@ -25,15 +25,17 @@ import { defineComponent, reactive, ref } from 'vue';
export default defineComponent({ export default defineComponent({
setup() { setup() {
const data = reactive(['Daily', 'Weekly', 'Monthly']); const data = reactive(['Daily', 'Weekly', 'Monthly']);
const isDisabled = ref<boolean>(false); const isDisabled = ref(false);
const loadMore = () => { const loadMore = () => {
data.push(...['Quarterly', 'Yearly']); data.push(...['Quarterly', 'Yearly']);
isDisabled.value = true; isDisabled.value = true;
}; };
const value = ref(data[0]);
return { return {
data, data,
loadMore, loadMore,
isDisabled, 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 /> <basic />
<block /> <block />
<disabled /> <disabled />
<controlled />
<custom /> <custom />
<dynamic /> <dynamic />
<size /> <size />
<icon />
</demo-sort> </demo-sort>
</template> </template>
@ -18,14 +16,12 @@ import US from '../index.en-US.md';
import Basic from './basic.vue'; import Basic from './basic.vue';
import Block from './block.vue'; import Block from './block.vue';
import Disabled from './disabled.vue'; import Disabled from './disabled.vue';
import Controlled from './controlled.vue';
import Custom from './custom.vue'; import Custom from './custom.vue';
import Dynamic from './dynamic.vue'; import Dynamic from './dynamic.vue';
import Size from './size.vue'; import Size from './size.vue';
import Icon from './icon.vue';
export default defineComponent({ export default defineComponent({
components: { Icon, Size, Dynamic, Custom, Controlled, Disabled, Block, Basic }, components: { Size, Dynamic, Custom, Disabled, Block, Basic },
category: 'Components', category: 'Components',
subtitle: '分段控制器', subtitle: '分段控制器',
type: 'Data Display', type: 'Data Display',

@ -14,23 +14,29 @@ title:
There are three sizes of an a-segmented: `large` (40px), `default` (32px) and `small` (24px). There are three sizes of an a-segmented: `large` (40px), `default` (32px) and `small` (24px).
</docs> </docs>
<template> <template>
<a-segmented :options="data" size="large" /> <a-segmented v-model:value="value" :options="data" size="large" />
<br /> <br />
<br /> <br />
<a-segmented :options="data" /> <a-segmented v-model:value="value2" :options="data" />
<br /> <br />
<br /> <br />
<a-segmented :options="data" size="small" /> <a-segmented v-model:value="value3" :options="data" size="small" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive } from 'vue'; import { defineComponent, ref, reactive } from 'vue';
export default defineComponent({ export default defineComponent({
setup() { setup() {
const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']); const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']);
const value = ref(data[0]);
const value2 = ref(data[0]);
const value3 = ref(data[0]);
return { return {
data, data,
value,
value2,
value3,
}; };
}, },
}); });

@ -13,14 +13,35 @@ Segmented Controls.
## API ## API
### Segmented
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| block | Option to fit width to its parent\'s width | boolean | false | | | block | Option to fit width to its parent\'s width | boolean | false | |
| defaultValue | Default selected value | string \| number | | |
| disabled | Disable all segments | boolean | false | | | 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[] \| SegmentedOption[] | [] | |
| options | Set children optional | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
| size | The size of the Segmented. | `large` \| `middle` \| `small` | - | | | size | The size of the Segmented. | `large` \| `middle` \| `small` | - | |
| value | Currently selected value | string \| number | | | | 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 Segmented from './src';
import type { SegmentedProps } from './src'; import type { SegmentedProps } from './src';
import { withInstall } from '../_util/type';
Segmented.install = function (app: App) { export default withInstall(Segmented);
app.component(Segmented.name, Segmented);
return app;
};
export default Segmented;
export type { SegmentedProps }; export type { SegmentedProps };

@ -19,9 +19,32 @@ title: Segmented
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| block | 将宽度调整为父元素宽度的选项 | boolean | 无 | | | block | 将宽度调整为父元素宽度的选项 | boolean | 无 | |
| defaultValue | 默认选中的值 | string \| number | | |
| disabled | 是否禁用 | boolean | false | | | disabled | 是否禁用 | boolean | false | |
| change | 选项变化时的回调函数 | function(value: string \| number) | | | | options | 数据化配置选项内容 | string[] \| number[] \| SegmentedOption[] | [] | |
| options | 数据化配置选项内容 | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
| size | 控件尺寸 | `large` \| `middle` \| `small` | - | | | size | 控件尺寸 | `large` \| `middle` \| `small` | - | |
| value | 当前选中的值 | string \| number | | | | 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 { defineComponent, ref, computed } from 'vue';
import type { ExtractPropTypes, PropType } from 'vue'; import type { ExtractPropTypes, FunctionalComponent } from 'vue';
import classNames from '../../_util/classNames'; import classNames from '../../_util/classNames';
import useConfigInject from '../../config-provider/hooks/useConfigInject'; 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 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 type segmentedSize = 'large' | 'small';
export interface SegmentedOptions { export interface SegmentedBaseOption {
value?: string; value: string | number;
disabled?: boolean; 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 = () => { export const segmentedProps = () => {
return { return {
options: { type: Array as PropType<Array<SegmentedOptions | string | number>> }, prefixCls: String,
defaultValue: { type: [Number, String] }, options: arrayType<(SegmentedOption | string | number)[]>(),
block: Boolean, block: booleanType(),
disabled: Boolean, disabled: booleanType(),
size: { type: String as PropType<segmentedSize> }, size: stringType<segmentedSize>(),
value: { ...someType<SegmentedValue>([String, Number]), required: true },
motionName: String,
}; };
}; };
export type SegmentedProps = Partial<ExtractPropTypes<ReturnType<typeof segmentedProps>>>; 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({ export default defineComponent({
name: 'ASegmented', name: 'ASegmented',
inheritAttrs: false, inheritAttrs: false,
props: { ...initDefaultProps(segmentedProps(), {}) }, props: initDefaultProps(segmentedProps(), {
emits: ['change', 'value'], options: [],
slots: ['icon', 'title'], motionName: 'thumb-motion',
setup(props, { emit, slots }) { }),
const { prefixCls } = useConfigInject('segmented', props); emits: ['change', 'update:value'],
slots: ['label'],
setup(props, { emit, slots, attrs }) {
const { prefixCls, direction, size } = useConfigInject('segmented', props);
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId] = useStyle(prefixCls);
const pre = prefixCls.value; const rootRef = ref<HTMLDivElement>();
const { size } = toRefs(props); const thumbShow = ref(false);
const itemRef = ref([]);
const { options, disabled, defaultValue } = toRefs(props); const segmentedOptions = computed(() => normalizeOptions(props.options));
const segmentedItemInput = () => { const handleChange = (_event: ChangeEvent, val: SegmentedValue) => {
return <input type="radio" class={`${pre}-item-input`} disabled checked />; if (props.disabled) {
}; return;
const isDisabled = item => {
if (disabled.value || (typeof item == 'object' && item.disabled)) {
return segmentedItemInput();
}
};
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>; emit('update:value', val);
}; emit('change', val);
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}
/>
) : (
''
);
}; };
return () => { return () => {
const pre = prefixCls.value;
return wrapSSR( return wrapSSR(
<div <div
class={classNames(pre, { {...attrs}
[hashId.value]: true, class={classNames(
[`${pre}-block`]: props.block, pre,
[`${pre}-item-disabled`]: props.disabled, {
[`${pre}-lg`]: size.value == 'large', [hashId.value]: true,
[`${pre}-sm`]: size.value == 'small', [`${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`)}> <div class={`${pre}-group`}>
{thumbNode()} <MotionThumb
{options.value.map((item, index) => { containerRef={rootRef}
return ( prefixCls={pre}
<label value={props.value}
ref={ref => (itemRef.value[index] = ref)} motionName={`${pre}-${props.motionName}`}
class={classNames(`${pre}-item`, { direction={direction.value}
[`${pre}-item-selected`]: currentItemKey.value == index, getValueIndex={val => segmentedOptions.value.findIndex(n => n.value === val)}
[`${pre}-item-disabled`]: disabled.value || isValueType(item), onMotionStart={() => {
})} thumbShow.value = true;
onClick={() => handleSelectedChange(item, index)} }}
> onMotionEnd={() => {
{isDisabled(item)} thumbShow.value = false;
<div class={classNames(`${pre}-item-label`)} key={index}> }}
{iconNode(index)} />
{typeof item == 'object' ? itemNode(item, index) : item} {segmentedOptions.value.map(segmentedOption => (
</div> <SegmentedOption
</label> 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>
</div>, </div>,
); );

@ -17,7 +17,7 @@ interface SegmentedToken extends FullToken<'Segmented'> {
} }
// ============================== Mixins ============================== // ============================== Mixins ==============================
function segmentedDisabledItem(cls: string, token: SegmentedToken): CSSObject { function getItemDisabledStyle(cls: string, token: SegmentedToken): CSSObject {
return { return {
[`${cls}, ${cls}:hover, ${cls}:focus`]: { [`${cls}, ${cls}:hover, ${cls}:focus`]: {
color: token.colorTextDisabled, 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 { return {
backgroundColor: token.bgColorSelected, backgroundColor: token.bgColorSelected,
boxShadow: token.boxShadow, boxShadow: token.boxShadow,
@ -39,8 +39,8 @@ const segmentedTextEllipsisCss: CSSObject = {
...textEllipsis, ...textEllipsis,
}; };
// ============================== Shared ============================== // ============================== Styles ==============================
const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObject => { const genSegmentedStyle: GenerateStyle<SegmentedToken> = (token: SegmentedToken) => {
const { componentCls } = token; const { componentCls } = token;
return { return {
@ -63,16 +63,16 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
}, },
// RTL styles // RTL styles
'&&-rtl': { [`&${componentCls}-rtl`]: {
direction: 'rtl', direction: 'rtl',
}, },
// block styles // block styles
'&&-block': { [`&${componentCls}-block`]: {
display: 'flex', display: 'flex',
}, },
[`&&-block ${componentCls}-item`]: { [`&${componentCls}-block ${componentCls}-item`]: {
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
}, },
@ -86,7 +86,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
borderRadius: token.borderRadiusSM, borderRadius: token.borderRadiusSM,
'&-selected': { '&-selected': {
...getSegmentedItemSelectedStyle(token), ...getItemSelectedStyle(token),
color: token.labelColorHover, color: token.labelColorHover,
}, },
@ -97,7 +97,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
height: '100%', height: '100%',
top: 0, top: 0,
insetInlineStart: 0, insetInlineStart: 0,
borderRadius: token.borderRadiusSM, borderRadius: 'inherit',
transition: `background-color ${token.motionDurationMid}`, transition: `background-color ${token.motionDurationMid}`,
}, },
@ -118,7 +118,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
// syntactic sugar to add `icon` for Segmented Item // syntactic sugar to add `icon` for Segmented Item
'&-icon + *': { '&-icon + *': {
marginInlineEnd: token.marginSM / 2, marginInlineStart: token.marginSM / 2,
}, },
'&-input': { '&-input': {
@ -132,8 +132,26 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
}, },
}, },
// thumb styles
[`${componentCls}-thumb`]: {
...getItemSelectedStyle(token),
position: 'absolute',
insetBlockStart: 0,
insetInlineStart: 0,
width: 0,
height: '100%',
padding: `${token.paddingXXS}px 0`,
borderRadius: token.borderRadiusSM,
[`& ~ ${componentCls}-item:not(${componentCls}-item-selected):not(${componentCls}-item-disabled)::after`]:
{
backgroundColor: 'transparent',
},
},
// size styles // size styles
'&&-lg': { [`&${componentCls}-lg`]: {
borderRadius: token.borderRadiusLG, borderRadius: token.borderRadiusLG,
[`${componentCls}-item-label`]: { [`${componentCls}-item-label`]: {
minHeight: token.controlHeightLG - token.segmentedContainerPadding * 2, minHeight: token.controlHeightLG - token.segmentedContainerPadding * 2,
@ -141,44 +159,26 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
padding: `0 ${token.segmentedPaddingHorizontal}px`, padding: `0 ${token.segmentedPaddingHorizontal}px`,
fontSize: token.fontSizeLG, fontSize: token.fontSizeLG,
}, },
[`${componentCls}-item-selected`]: { [`${componentCls}-item, ${componentCls}-thumb`]: {
borderRadius: token.borderRadius, borderRadius: token.borderRadius,
}, },
}, },
'&&-sm': { [`&${componentCls}-sm`]: {
borderRadius: token.borderRadiusSM, borderRadius: token.borderRadiusSM,
[`${componentCls}-item-label`]: { [`${componentCls}-item-label`]: {
minHeight: token.controlHeightSM - token.segmentedContainerPadding * 2, minHeight: token.controlHeightSM - token.segmentedContainerPadding * 2,
lineHeight: `${token.controlHeightSM - token.segmentedContainerPadding * 2}px`, lineHeight: `${token.controlHeightSM - token.segmentedContainerPadding * 2}px`,
padding: `0 ${token.segmentedPaddingHorizontalSM}px`, padding: `0 ${token.segmentedPaddingHorizontalSM}px`,
}, },
[`${componentCls}-item-selected`]: { [`${componentCls}-item, ${componentCls}-thumb`]: {
borderRadius: token.borderRadiusXS, borderRadius: token.borderRadiusXS,
}, },
}, },
// disabled styles // disabled styles
...segmentedDisabledItem(`&-disabled ${componentCls}-item`, token), ...getItemDisabledStyle(`&-disabled ${componentCls}-item`, token),
...segmentedDisabledItem(`${componentCls}-item-disabled`, token), ...getItemDisabledStyle(`${componentCls}-item-disabled`, token),
// thumb styles
[`${componentCls}-thumb`]: {
...getSegmentedItemSelectedStyle(token),
position: 'absolute',
insetBlockStart: 0,
insetInlineStart: 0,
width: 0,
height: '100%',
padding: `${token.paddingXXS}px 0`,
borderRadius: token.borderRadiusSM,
[`& ~ ${componentCls}-item:not(${componentCls}-item-selected):not(${componentCls}-item-disabled)::after`]:
{
backgroundColor: 'transparent',
},
},
// transition effect when `appear-active` // transition effect when `appear-active`
[`${componentCls}-thumb-motion-appear-active`]: { [`${componentCls}-thumb-motion-appear-active`]: {
@ -188,6 +188,7 @@ const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObjec
}, },
}; };
}; };
// ============================== Export ============================== // ============================== Export ==============================
export default genComponentStyleHook('Segmented', token => { export default genComponentStyleHook('Segmented', token => {
const { const {
@ -210,5 +211,5 @@ export default genComponentStyleHook('Segmented', token => {
bgColorHover: colorFillSecondary, bgColorHover: colorFillSecondary,
bgColorSelected: colorBgElevated, bgColorSelected: colorBgElevated,
}); });
return [genSharedSegmentedStyle(segmentedToken)]; return [genSegmentedStyle(segmentedToken)];
}); });

@ -163,6 +163,8 @@ declare module 'vue' {
ASelect: typeof import('ant-design-vue')['Select']; ASelect: typeof import('ant-design-vue')['Select'];
ASegmented: typeof import('ant-design-vue')['Segmented'];
ASelectOptGroup: typeof import('ant-design-vue')['SelectOptGroup']; ASelectOptGroup: typeof import('ant-design-vue')['SelectOptGroup'];
ASelectOption: typeof import('ant-design-vue')['SelectOption']; ASelectOption: typeof import('ant-design-vue')['SelectOption'];

Loading…
Cancel
Save