feat: add segmented (#6286)
parent
47385347ee
commit
62e7f94aba
|
@ -244,3 +244,7 @@ export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from
|
||||||
export { default as Upload, UploadDragger } from './upload';
|
export { default as Upload, UploadDragger } from './upload';
|
||||||
|
|
||||||
export { default as LocaleProvider } from './locale-provider';
|
export { default as LocaleProvider } from './locale-provider';
|
||||||
|
|
||||||
|
export type { SegmentedProps } from './segmented';
|
||||||
|
|
||||||
|
export { default as Segmented } from './segmented';
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import demoTest from '../../../tests/shared/demoTest';
|
||||||
|
|
||||||
|
demoTest('segmented');
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import Segmented from '../index';
|
||||||
|
describe('Segmented', () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
render() {
|
||||||
|
return <Segmented></Segmented>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const todo = wrapper.get('[options="[1,2,3,4,5]"]');
|
||||||
|
expect(todo.text()).toBe('segmented');
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 0
|
||||||
|
title:
|
||||||
|
zh-CN: 基本用法
|
||||||
|
en-US: Basic Usage
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
最简单的用法。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
The most basic usage.
|
||||||
|
</docs>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-segmented :options="data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 1
|
||||||
|
title:
|
||||||
|
zh-CN: Block分段控制器
|
||||||
|
en-US: Block Segmented
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
`block` 属性使其适合父元素宽度。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
`block` property will make the `Segmented` fit to its parent width.
|
||||||
|
</docs>
|
||||||
|
<template>
|
||||||
|
<a-segmented block :options="data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const data = reactive([123, 456, 'longtext-longtext-longtext-longtext']);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,94 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 5
|
||||||
|
title:
|
||||||
|
zh-CN: 自定义渲染
|
||||||
|
en-US: Custom
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
自定义渲染每一个 Segmented Item。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
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-avatar>
|
||||||
|
<div>User 3</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</a-segmented>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive } 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' },
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
options2,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 3
|
||||||
|
title:
|
||||||
|
zh-CN: 不可用
|
||||||
|
en-US: Disabled
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
Segmented 不可用。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
Disabled Segmented.
|
||||||
|
</docs>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-segmented disabled :options="data" />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-segmented :options="data2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const data = reactive(['Map', 'Transit', 'Satellite']);
|
||||||
|
const data2 = reactive([
|
||||||
|
'Daily',
|
||||||
|
{ value: 'Weekly', disabled: true },
|
||||||
|
'Monthly',
|
||||||
|
{ value: 'Quarterly', disabled: true },
|
||||||
|
'Yearly',
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
data2,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 6
|
||||||
|
title:
|
||||||
|
zh-CN: 动态数据
|
||||||
|
en-US: Dynamic
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
动态加载数据。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
Load dynamically.
|
||||||
|
</docs>
|
||||||
|
<template>
|
||||||
|
<a-segmented :options="data"></a-segmented>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-button type="primary" @click="loadMore" :disabled="isDisabled">Load More</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref } from 'vue';
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const data = reactive(['Daily', 'Weekly', 'Monthly']);
|
||||||
|
const isDisabled = ref<boolean>(false);
|
||||||
|
const loadMore = () => {
|
||||||
|
data.push(...['Quarterly', 'Yearly']);
|
||||||
|
isDisabled.value = true;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
loadMore,
|
||||||
|
isDisabled,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,49 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<demo-sort>
|
||||||
|
<basic />
|
||||||
|
<block />
|
||||||
|
<disabled />
|
||||||
|
<controlled />
|
||||||
|
<custom />
|
||||||
|
<dynamic />
|
||||||
|
<size />
|
||||||
|
<icon />
|
||||||
|
</demo-sort>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import CN from '../index.zh-CN.md';
|
||||||
|
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 },
|
||||||
|
category: 'Components',
|
||||||
|
subtitle: '分段控制器',
|
||||||
|
type: 'Data Display',
|
||||||
|
title: 'Segmented',
|
||||||
|
CN,
|
||||||
|
US,
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 6
|
||||||
|
title:
|
||||||
|
zh-CN: 三种大小
|
||||||
|
en-US: Three sizes of Segmented
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
我们为 `<a-segmented />` 组件定义了三种尺寸(大、默认、小),高度分别为 `40px`、`32px` 和 `24px`。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
There are three sizes of an a-segmented: `large` (40px), `default` (32px) and `small` (24px).
|
||||||
|
</docs>
|
||||||
|
<template>
|
||||||
|
<a-segmented :options="data" size="large" />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-segmented :options="data" />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-segmented :options="data" size="small" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const data = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
category: Components
|
||||||
|
type: Data Display
|
||||||
|
title: Segmented
|
||||||
|
---
|
||||||
|
|
||||||
|
Segmented Controls.
|
||||||
|
|
||||||
|
### When To Use
|
||||||
|
|
||||||
|
- When displaying multiple options and user can select a single option;
|
||||||
|
- When switching the selected option, the content of the associated area changes.
|
||||||
|
|
||||||
|
## 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 }> | [] | |
|
||||||
|
| size | The size of the Segmented. | `large` \| `middle` \| `small` | - | |
|
||||||
|
| value | Currently selected value | string \| number | | |
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { App } from 'vue';
|
||||||
|
import Segmented from './src';
|
||||||
|
import type { SegmentedProps } from './src';
|
||||||
|
|
||||||
|
Segmented.install = function (app: App) {
|
||||||
|
app.component(Segmented.name, Segmented);
|
||||||
|
return app;
|
||||||
|
};
|
||||||
|
export default Segmented;
|
||||||
|
export type { SegmentedProps };
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
category: Components
|
||||||
|
subtitle: 分段控制器
|
||||||
|
type: 数据展示
|
||||||
|
title: Segmented
|
||||||
|
---
|
||||||
|
|
||||||
|
分段控制器。
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
- 用于展示多个选项并允许用户选择其中单个选项;
|
||||||
|
- 当切换选中选项时,关联区域的内容会发生变化。
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Segmented
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| block | 将宽度调整为父元素宽度的选项 | boolean | 无 | |
|
||||||
|
| defaultValue | 默认选中的值 | string \| number | | |
|
||||||
|
| disabled | 是否禁用 | boolean | false | |
|
||||||
|
| change | 选项变化时的回调函数 | function(value: string \| number) | | |
|
||||||
|
| options | 数据化配置选项内容 | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
|
||||||
|
| size | 控件尺寸 | `large` \| `middle` \| `small` | - | |
|
||||||
|
| value | 当前选中的值 | string \| number | | |
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Segmented from './segmented';
|
||||||
|
import type { SegmentedProps } from './segmented';
|
||||||
|
|
||||||
|
export type { SegmentedProps };
|
||||||
|
export default Segmented;
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { defineComponent, ref, toRefs, reactive, watch } from 'vue';
|
||||||
|
import type { ExtractPropTypes, PropType } from 'vue';
|
||||||
|
import classNames from '../../_util/classNames';
|
||||||
|
import useConfigInject from '../../config-provider/hooks/useConfigInject';
|
||||||
|
import { getPropsSlot, initDefaultProps } from '../../_util/props-util';
|
||||||
|
import useStyle from '../style';
|
||||||
|
|
||||||
|
export type segmentedSize = 'large' | 'small';
|
||||||
|
export interface SegmentedOptions {
|
||||||
|
value?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
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> },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type SegmentedProps = Partial<ExtractPropTypes<ReturnType<typeof segmentedProps>>>;
|
||||||
|
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);
|
||||||
|
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 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}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
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',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,214 @@
|
||||||
|
import type { CSSObject } from '../../_util/cssinjs';
|
||||||
|
import type { FullToken, GenerateStyle } from '../../theme/internal';
|
||||||
|
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
|
||||||
|
import { resetComponent, textEllipsis } from '../../_style';
|
||||||
|
|
||||||
|
export interface ComponentToken {}
|
||||||
|
|
||||||
|
interface SegmentedToken extends FullToken<'Segmented'> {
|
||||||
|
segmentedPaddingHorizontal: number;
|
||||||
|
segmentedPaddingHorizontalSM: number;
|
||||||
|
segmentedContainerPadding: number;
|
||||||
|
labelColor: string;
|
||||||
|
labelColorHover: string;
|
||||||
|
bgColor: string;
|
||||||
|
bgColorHover: string;
|
||||||
|
bgColorSelected: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Mixins ==============================
|
||||||
|
function segmentedDisabledItem(cls: string, token: SegmentedToken): CSSObject {
|
||||||
|
return {
|
||||||
|
[`${cls}, ${cls}:hover, ${cls}:focus`]: {
|
||||||
|
color: token.colorTextDisabled,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSegmentedItemSelectedStyle(token: SegmentedToken): CSSObject {
|
||||||
|
return {
|
||||||
|
backgroundColor: token.bgColorSelected,
|
||||||
|
boxShadow: token.boxShadow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentedTextEllipsisCss: CSSObject = {
|
||||||
|
overflow: 'hidden',
|
||||||
|
// handle text ellipsis
|
||||||
|
...textEllipsis,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================== Shared ==============================
|
||||||
|
const genSharedSegmentedStyle: GenerateStyle<SegmentedToken> = (token): CSSObject => {
|
||||||
|
const { componentCls } = token;
|
||||||
|
|
||||||
|
return {
|
||||||
|
[componentCls]: {
|
||||||
|
...resetComponent(token),
|
||||||
|
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: token.segmentedContainerPadding,
|
||||||
|
color: token.labelColor,
|
||||||
|
backgroundColor: token.bgColor,
|
||||||
|
borderRadius: token.borderRadius,
|
||||||
|
transition: `all ${token.motionDurationMid} ${token.motionEaseInOut}`,
|
||||||
|
|
||||||
|
[`${componentCls}-group`]: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
justifyItems: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
// RTL styles
|
||||||
|
'&&-rtl': {
|
||||||
|
direction: 'rtl',
|
||||||
|
},
|
||||||
|
|
||||||
|
// block styles
|
||||||
|
'&&-block': {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
|
||||||
|
[`&&-block ${componentCls}-item`]: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// item styles
|
||||||
|
[`${componentCls}-item`]: {
|
||||||
|
position: 'relative',
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: `color ${token.motionDurationMid} ${token.motionEaseInOut}`,
|
||||||
|
borderRadius: token.borderRadiusSM,
|
||||||
|
|
||||||
|
'&-selected': {
|
||||||
|
...getSegmentedItemSelectedStyle(token),
|
||||||
|
color: token.labelColorHover,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
borderRadius: token.borderRadiusSM,
|
||||||
|
transition: `background-color ${token.motionDurationMid}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
[`&:hover:not(${componentCls}-item-selected):not(${componentCls}-item-disabled)`]: {
|
||||||
|
color: token.labelColorHover,
|
||||||
|
|
||||||
|
'&::after': {
|
||||||
|
backgroundColor: token.bgColorHover,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'&-label': {
|
||||||
|
minHeight: token.controlHeight - token.segmentedContainerPadding * 2,
|
||||||
|
lineHeight: `${token.controlHeight - token.segmentedContainerPadding * 2}px`,
|
||||||
|
padding: `0 ${token.segmentedPaddingHorizontal}px`,
|
||||||
|
...segmentedTextEllipsisCss,
|
||||||
|
},
|
||||||
|
|
||||||
|
// syntactic sugar to add `icon` for Segmented Item
|
||||||
|
'&-icon + *': {
|
||||||
|
marginInlineEnd: token.marginSM / 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&-input': {
|
||||||
|
position: 'absolute',
|
||||||
|
insetBlockStart: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
|
||||||
|
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`
|
||||||
|
[`${componentCls}-thumb-motion-appear-active`]: {
|
||||||
|
transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOut}, width ${token.motionDurationSlow} ${token.motionEaseInOut}`,
|
||||||
|
willChange: 'transform, width',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// ============================== Export ==============================
|
||||||
|
export default genComponentStyleHook('Segmented', token => {
|
||||||
|
const {
|
||||||
|
lineWidthBold,
|
||||||
|
lineWidth,
|
||||||
|
colorTextLabel,
|
||||||
|
colorText,
|
||||||
|
colorFillSecondary,
|
||||||
|
colorBgLayout,
|
||||||
|
colorBgElevated,
|
||||||
|
} = token;
|
||||||
|
|
||||||
|
const segmentedToken = mergeToken<SegmentedToken>(token, {
|
||||||
|
segmentedPaddingHorizontal: token.controlPaddingHorizontal - lineWidth,
|
||||||
|
segmentedPaddingHorizontalSM: token.controlPaddingHorizontalSM - lineWidth,
|
||||||
|
segmentedContainerPadding: lineWidthBold,
|
||||||
|
labelColor: colorTextLabel,
|
||||||
|
labelColorHover: colorText,
|
||||||
|
bgColor: colorBgLayout,
|
||||||
|
bgColorHover: colorFillSecondary,
|
||||||
|
bgColorSelected: colorBgElevated,
|
||||||
|
});
|
||||||
|
return [genSharedSegmentedStyle(segmentedToken)];
|
||||||
|
});
|
|
@ -49,6 +49,7 @@ import type { ComponentToken as UploadComponentToken } from '../../upload/style'
|
||||||
// import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
|
// import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
|
||||||
// import type { ComponentToken as AppComponentToken } from '../../app/style';
|
// import type { ComponentToken as AppComponentToken } from '../../app/style';
|
||||||
// import type { ComponentToken as WaveToken } from '../../_util/wave/style';
|
// import type { ComponentToken as WaveToken } from '../../_util/wave/style';
|
||||||
|
import type { ComponentToken as SegmentedComponentToken } from '../../segmented/style';
|
||||||
|
|
||||||
export interface ComponentTokenMap {
|
export interface ComponentTokenMap {
|
||||||
Affix?: {};
|
Affix?: {};
|
||||||
|
@ -118,4 +119,5 @@ export interface ComponentTokenMap {
|
||||||
|
|
||||||
// /** @private Internal TS definition. Do not use. */
|
// /** @private Internal TS definition. Do not use. */
|
||||||
// Wave?: WaveToken;
|
// Wave?: WaveToken;
|
||||||
|
Segmented?: SegmentedComponentToken;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue