diff --git a/components/components.ts b/components/components.ts index 020c5add2a..0b3f182008 100644 --- a/components/components.ts +++ b/components/components.ts @@ -244,3 +244,6 @@ export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from export { default as Upload, UploadDragger } from './upload'; export { default as LocaleProvider } from './locale-provider'; + +export type { SegmentedProps } from './segmented'; +export { default as Segmented } from './segmented'; diff --git a/components/segmented/__tests__/__snapshots__/demo.test.js.snap b/components/segmented/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 0000000000..b62795b48f --- /dev/null +++ b/components/segmented/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/segmented/demo/basic.vue correctly 1`] = ` +
+
+
+
+
+`; + +exports[`renders ./components/segmented/demo/block.vue correctly 1`] = ` +
+
+
+
+
+`; + +exports[`renders ./components/segmented/demo/controlled.vue correctly 1`] = ` +
+
+
+
+
+
+
+`; + +exports[`renders ./components/segmented/demo/custom.vue correctly 1`] = ` +
+
+`; + +exports[`renders ./components/segmented/demo/disabled.vue correctly 1`] = ` +
+
+
+
+
+


+
+
+
+
+
+
+`; + +exports[`renders ./components/segmented/demo/dynamic.vue correctly 1`] = ` +
+
+
+
+
+
+
+ +`; + +exports[`renders ./components/segmented/demo/icon.vue correctly 1`] = ` +
+
+
+
+
+`; + +exports[`renders ./components/segmented/demo/size.vue correctly 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/components/segmented/__tests__/demo.test.js b/components/segmented/__tests__/demo.test.js new file mode 100644 index 0000000000..6a3124c88b --- /dev/null +++ b/components/segmented/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('segmented'); diff --git a/components/segmented/__tests__/index.test.js b/components/segmented/__tests__/index.test.js new file mode 100644 index 0000000000..161bbefb2e --- /dev/null +++ b/components/segmented/__tests__/index.test.js @@ -0,0 +1,11 @@ +import { mount } from '@vue/test-utils'; +import Segmented from '../index'; +describe('Segmented', () => { + const wrapper = mount({ + render() { + return ; + }, + }); + const todo = wrapper.get('[options="[1,2,3,4,5]"]'); + expect(todo.text()).toBe('segmented'); +}); diff --git a/components/segmented/demo/basic.vue b/components/segmented/demo/basic.vue new file mode 100644 index 0000000000..2294600115 --- /dev/null +++ b/components/segmented/demo/basic.vue @@ -0,0 +1,32 @@ + +--- +order: 0 +title: + zh-CN: 基本用法 + en-US: Basic Usage +--- + +## zh-CN + +最简单的用法。 + +## en-US +The most basic usage. + + + + + diff --git a/components/segmented/demo/block.vue b/components/segmented/demo/block.vue new file mode 100644 index 0000000000..4a94c9e9eb --- /dev/null +++ b/components/segmented/demo/block.vue @@ -0,0 +1,31 @@ + +--- +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. + + + + diff --git a/components/segmented/demo/controlled.vue b/components/segmented/demo/controlled.vue new file mode 100644 index 0000000000..6023150480 --- /dev/null +++ b/components/segmented/demo/controlled.vue @@ -0,0 +1,35 @@ + +--- +order: 4 +title: + zh-CN: 受控模式 + en-US: Controlled mode +--- + +## zh-CN + +受控的 Segmented + +## en-US +Controlled Segmented. + + + + diff --git a/components/segmented/demo/custom.vue b/components/segmented/demo/custom.vue new file mode 100644 index 0000000000..09165d99d0 --- /dev/null +++ b/components/segmented/demo/custom.vue @@ -0,0 +1,94 @@ + +--- +order: 5 +title: + zh-CN: 自定义渲染 + en-US: Custom +--- + +## zh-CN + +自定义渲染每一个 Segmented Item。 + +## en-US +Custom each Segmented Item. + + + + diff --git a/components/segmented/demo/disabled.vue b/components/segmented/demo/disabled.vue new file mode 100644 index 0000000000..ac7ed75ccc --- /dev/null +++ b/components/segmented/demo/disabled.vue @@ -0,0 +1,44 @@ + +--- +order: 3 +title: + zh-CN: 不可用 + en-US: Disabled +--- + +## zh-CN + +Segmented 不可用。 + +## en-US +Disabled Segmented. + + + + diff --git a/components/segmented/demo/dynamic.vue b/components/segmented/demo/dynamic.vue new file mode 100644 index 0000000000..b888fa3d2b --- /dev/null +++ b/components/segmented/demo/dynamic.vue @@ -0,0 +1,40 @@ + +--- +order: 6 +title: + zh-CN: 动态数据 + en-US: Dynamic +--- + +## zh-CN + +动态加载数据。 + +## en-US +Load dynamically. + + + + diff --git a/components/segmented/demo/icon.vue b/components/segmented/demo/icon.vue new file mode 100644 index 0000000000..b486851583 --- /dev/null +++ b/components/segmented/demo/icon.vue @@ -0,0 +1,49 @@ + +--- +order: 7 +title: + zh-CN: 设置图标 + en-US: With Icon +--- + +## zh-CN + +给 Segmented Item 设置 Icon。 + +## en-US +Set `icon` for Segmented Item. + + + + diff --git a/components/segmented/demo/index.vue b/components/segmented/demo/index.vue new file mode 100644 index 0000000000..a43e7e60c7 --- /dev/null +++ b/components/segmented/demo/index.vue @@ -0,0 +1,36 @@ + + + diff --git a/components/segmented/demo/size.vue b/components/segmented/demo/size.vue new file mode 100644 index 0000000000..b22197f1aa --- /dev/null +++ b/components/segmented/demo/size.vue @@ -0,0 +1,37 @@ + +--- +order: 6 +title: + zh-CN: 三种大小 + en-US: Three sizes of Segmented +--- + +## zh-CN + +我们为 `` 组件定义了三种尺寸(大、默认、小),高度分别为 `40px`、`32px` 和 `24px`。 + +## en-US +There are three sizes of an a-segmented: `large` (40px), `default` (32px) and `small` (24px). + + + + diff --git a/components/segmented/index.en-US.md b/components/segmented/index.en-US.md new file mode 100644 index 0000000000..9a22d29f86 --- /dev/null +++ b/components/segmented/index.en-US.md @@ -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 | | | diff --git a/components/segmented/index.ts b/components/segmented/index.ts new file mode 100644 index 0000000000..02d21bc86b --- /dev/null +++ b/components/segmented/index.ts @@ -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 }; diff --git a/components/segmented/index.zh-CN.md b/components/segmented/index.zh-CN.md new file mode 100644 index 0000000000..70faa863a7 --- /dev/null +++ b/components/segmented/index.zh-CN.md @@ -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 | | | diff --git a/components/segmented/src/index.ts b/components/segmented/src/index.ts new file mode 100644 index 0000000000..4c8b46ef8f --- /dev/null +++ b/components/segmented/src/index.ts @@ -0,0 +1,5 @@ +import Segmented from './segmented'; +import type { SegmentedProps } from './segmented'; + +export type { SegmentedProps }; +export default Segmented; diff --git a/components/segmented/src/segmented.tsx b/components/segmented/src/segmented.tsx new file mode 100644 index 0000000000..1da88d0859 --- /dev/null +++ b/components/segmented/src/segmented.tsx @@ -0,0 +1,150 @@ +import { defineComponent, ref, toRefs, reactive, watch } from 'vue'; +import type { ExtractPropTypes, PropType } from 'vue'; +import classNames from '../../_util/classNames'; +import useConfigInject from '../../_util/hooks/useConfigInject'; +import { getPropsSlot, initDefaultProps } from '../../_util/props-util'; +export type segmentedSize = 'large' | 'small'; +export interface SegmentedOptions { + value?: string; + disabled?: boolean; +} +export const segmentedProps = () => { + return { + options: { type: Array as PropType> }, + defaultValue: { type: [Number, String] }, + block: Boolean, + disabled: Boolean, + size: { type: String as PropType }, + }; +}; +export type SegmentedProps = Partial>>; +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 pre = prefixCls.value; + const { size } = toRefs(props); + const itemRef = ref([]); + const { options, disabled, defaultValue } = toRefs(props); + const segmentedItemInput = () => { + 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 ? ( + {slots.icon?.(index)} + ) : ( + '' + ); + }; + const itemNode = (item, index) => { + if (title) { + return
{slots.title?.(index)}
; + } + return {item.value}; + }; + 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 ? ( +
+ ) : ( + '' + ); + }; + return () => { + return ( +
+
+ {thumbNode()} + {options.value.map((item, index) => { + return ( + + ); + })} +
+
+ ); + }; + }, +}); diff --git a/components/segmented/style/index.less b/components/segmented/style/index.less new file mode 100644 index 0000000000..6e88b3cc3b --- /dev/null +++ b/components/segmented/style/index.less @@ -0,0 +1,148 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; +@segmented-prefix-cls: ~'@{ant-prefix}-segmented'; +.@{segmented-prefix-cls} { + box-sizing: border-box; + margin: 0; + padding: 2px; + color: rgba(0, 0, 0, 0.65); + font-size: 14px; + line-height: 1.5714285714285714; + list-style: none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + display: inline-block; + background-color: #f5f5f5; + border-radius: 6px; + transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); + &-group { + position: relative; + display: flex; + align-items: stretch; + justify-items: flex-start; + width: 100%; + } + &-rtl { + direction: rtl; + } + &-block { + display: flex; + width: 100%; + .@{segmented-prefix-cls}-item { + flex: 1; + min-width: 0; + } + } + &-item { + position: relative; + text-align: center; + cursor: pointer; + transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); + border-radius: 4px; + user-select: none; + &::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + inset-inline-start: 0; + border-radius: 4px; + transition: background-color 0.2s; + } + &:hover:not(.@{segmented-prefix-cls}-item-selected):not( + .@{segmented-prefix-cls}-item-disabled + ) { + color: rgba(0, 0, 0, 0.88); + } + &:hover:not(.@{segmented-prefix-cls}-item-selected):not( + .@{segmented-prefix-cls}-item-disabled + )::after { + background-color: rgba(0, 0, 0, 0.06); + } + } + &-item-selected { + background-color: #ffffff; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), + 0 2px 4px 0 rgba(0, 0, 0, 0.02); + color: rgba(0, 0, 0, 0.88); + } + &-item-label { + min-height: 28px; + line-height: 28px; + padding: 0 11px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + &-item-icon { + margin-inline-end: 6px; + } + &-item-input { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; + } + &-lg { + border-radius: 8px; + .@{segmented-prefix-cls}-item-label { + min-height: 36px; + line-height: 36px; + padding: 0 11px; + font-size: 16px; + } + .@{segmented-prefix-cls}-item-selected { + border-radius: 6px; + } + } + &-sm { + border-radius: 4px; + .@{segmented-prefix-cls}-item-label { + min-height: 20px; + line-height: 20px; + padding: 0 7px; + } + .@{segmented-prefix-cls}-item-selected { + border-radius: 2px; + } + } + &-disabled &-item, + &-disabled &-item:hover, + &-disabled &-item:focus { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; + } + &-item-disabled, + &-item-disabled:hover, + &-item-disabled:focus { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; + } + &-thumb { + background-color: #ffffff; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), + 0 2px 4px 0 rgba(0, 0, 0, 0.02); + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + width: 0; + height: 100%; + padding: 4px 0; + border-radius: 4px; + .@{segmented-prefix-cls}-item:not(.@{segmented-prefix-cls}-item-selected):not( + .@{segmented-prefix-cls}-item-disabled + )::after { + background-color: #ffffff; + } + } + &-thumb-motion-appear-active { + transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + will-change: transform, width; + } +} diff --git a/components/segmented/style/index.tsx b/components/segmented/style/index.tsx new file mode 100644 index 0000000000..d74e52ee9f --- /dev/null +++ b/components/segmented/style/index.tsx @@ -0,0 +1 @@ +import './index.less'; diff --git a/components/style.ts b/components/style.ts index 88bd444b59..d4390302fa 100644 --- a/components/style.ts +++ b/components/style.ts @@ -62,3 +62,4 @@ import './space/style'; import './image/style'; import './typography/style'; // import './color-picker/style'; +import './segmented/style'; diff --git a/site/src/demo.js b/site/src/demo.js index ff3209fa1f..b249c06118 100644 --- a/site/src/demo.js +++ b/site/src/demo.js @@ -395,4 +395,10 @@ export default { // type: 'Data Entry', // title: 'ColorPicker (Beta)', // }, + segmented: { + category: 'Components', + subtitle: '分段控制器', + type: 'Data Display', + title: 'Segmented', + }, };