From 101567dc30d3a25d7afaca8354dfdfcf423d7f49 Mon Sep 17 00:00:00 2001 From: vagusx Date: Mon, 5 Aug 2024 14:29:56 +0800 Subject: [PATCH 01/12] feat: new component Segmented --- config/components.ts | 1 + package.json | 1 + src/components/segmented/demos/demo1.tsx | 174 +++++ src/components/segmented/index.en.md | 47 ++ src/components/segmented/index.ts | 6 + src/components/segmented/index.zh.md | 45 ++ src/components/segmented/segmented.less | 116 +++ src/components/segmented/segmented.tsx | 111 +++ .../__snapshots__/segmented.test.tsx.snap | 705 ++++++++++++++++++ .../segmented/tests/segmented.test.tsx | 349 +++++++++ src/index.ts | 2 + 11 files changed, 1557 insertions(+) create mode 100644 src/components/segmented/demos/demo1.tsx create mode 100644 src/components/segmented/index.en.md create mode 100644 src/components/segmented/index.ts create mode 100644 src/components/segmented/index.zh.md create mode 100644 src/components/segmented/segmented.less create mode 100644 src/components/segmented/segmented.tsx create mode 100644 src/components/segmented/tests/__snapshots__/segmented.test.tsx.snap create mode 100644 src/components/segmented/tests/segmented.test.tsx diff --git a/config/components.ts b/config/components.ts index 534e13e71a..aa725f712f 100644 --- a/config/components.ts +++ b/config/components.ts @@ -27,6 +27,7 @@ export const components = { '/components/infinite-scroll', '/components/list', '/components/page-indicator', + '/components/segmented', '/components/steps', '/components/swiper', '/components/tag', diff --git a/package.json b/package.json index 3cb741bc95..efe484ce77 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "deepmerge": "^4.3.1", "nano-memoize": "^3.0.16", "rc-field-form": "^1.34.2", + "rc-segmented": "~2.3.0", "rc-util": "^5.38.1", "react-fast-compare": "^3.2.2", "react-is": "^18.2.0", diff --git a/src/components/segmented/demos/demo1.tsx b/src/components/segmented/demos/demo1.tsx new file mode 100644 index 0000000000..790f8d6bde --- /dev/null +++ b/src/components/segmented/demos/demo1.tsx @@ -0,0 +1,174 @@ +import React, { useState } from 'react' +import { Avatar, Button, Segmented } from 'antd-mobile' +import { AppOutline, UnorderedListOutline } from 'antd-mobile-icons' +import { DemoBlock } from 'demos' + +const defaultOptions = ['Daily', 'Weekly', 'Monthly'] + +export default () => { + const [value, setValue] = useState('Map') + + const [options, setOptions] = useState(defaultOptions) + const [moreLoaded, setMoreLoaded] = useState(false) + + const handleLoadOptions = () => { + setOptions([...defaultOptions, 'Quarterly', 'Yearly']) + setMoreLoaded(true) + } + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + , + }, + { + label: 'Kanban', + value: 'Kanban', + icon: , + }, + ]} + /> + + + + , + }, + { + value: 'Kanban', + icon: , + }, + ]} + /> + + + + + +
User 1
+ + ), + value: 'user1', + }, + { + label: ( +
+ +
User 2
+
+ ), + value: 'user2', + }, + { + label: ( +
+ +
User 3
+
+ ), + value: 'user3', + }, + ]} + /> +
+ + + +
Spring
+
Jan-Mar
+ + ), + value: 'spring', + }, + { + label: ( +
+
Summer
+
Apr-Jun
+
+ ), + value: 'summer', + }, + { + label: ( +
+
Autumn
+
Jul-Sept
+
+ ), + value: 'autumn', + }, + { + label: ( +
+
Winter
+
Oct-Dec
+
+ ), + value: 'winter', + }, + ]} + /> +
+ + ) +} diff --git a/src/components/segmented/index.en.md b/src/components/segmented/index.en.md new file mode 100644 index 0000000000..fd6ae43abb --- /dev/null +++ b/src/components/segmented/index.en.md @@ -0,0 +1,47 @@ +# Segmented + +This component is available since `antd-mobile@5.34.0`. + +## 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. + +## Demos + + + +## Segmented + +### Props + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| block | Option to fit width to its parent\'s width | boolean | false | +| defaultValue | Default selected value | string \| number | | +| disabled | Disable all segments | boolean | false | +| onChange | The callback function that is triggered when the state changes | function(value: string \| number) | | +| options | Set children optional | string\[] \| number\[] \| SegmentedItemType\[] | [] | +| value | Currently selected value | string \| number | | + +### SegmentedItemType + +| Property | Description | Type | Default | +| --------- | -------------------------------- | ---------------- | ------- | +| label | Display text for Segmented item | ReactNode | - | +| value | Value for Segmented item | string \| number | - | +| icon | Display icon for Segmented item | ReactNode | - | +| disabled | Disabled state of segmented item | boolean | false | +| className | The additional css class | string | - | + +### CSS Variables + +| Name | Description | Default | +| --- | --- | --- | +| Name | Description | Default | +| ------------------- | ---------------------- | ----------------------- | +| --background-color | Background color | `var(--adm-color-fill-content)` | +| --segment-selected-bg | Background color of selected segment | `var(--adm-color-background)` | +| --segment-color | Text color of segment item | `var(--adm-color-text-secondary)` | +| --segment-selected-color | Text color of selected segment item | `var(--adm-color-text)` | +| --segment-disabled-color | Text color of disabled segment item | `var(--adm-color-weak)` | diff --git a/src/components/segmented/index.ts b/src/components/segmented/index.ts new file mode 100644 index 0000000000..35d6b0bf72 --- /dev/null +++ b/src/components/segmented/index.ts @@ -0,0 +1,6 @@ +import './segmented.less' +import { Segmented } from './segmented' + +export type { SegmentedProps, SegmentedValue } from './segmented' + +export default Segmented diff --git a/src/components/segmented/index.zh.md b/src/components/segmented/index.zh.md new file mode 100644 index 0000000000..47ff678326 --- /dev/null +++ b/src/components/segmented/index.zh.md @@ -0,0 +1,45 @@ +# Segmented 分段控制器 + +分段控制器。自 `antd-mobile@5.34.0` 版本开始提供该组件。 + +## 何时使用 + +- 用于展示多个选项并允许用户选择其中单个选项; +- 当切换选中选项时,关联区域的内容会发生变化。 + +## 示例 + + + +## Segmented + +### 属性 + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| block | 将宽度调整为父元素宽度的选项 | boolean | false | +| defaultValue | 默认选中的值 | string \| number | | +| disabled | 是否禁用 | boolean | false | +| onChange | 选项变化时的回调函数 | function(value: string \| number) | | +| options | 数据化配置选项内容 | string\[] \| number\[] \| SegmentedItemType\[] | [] | +| value | 当前选中的值 | string \| number | | + +### SegmentedItemType + +| 属性 | 描述 | 类型 | 默认值 | 版本 | +| --------- | ---------------- | ---------------- | ------ | ---- | +| label | 分段项的显示文本 | ReactNode | - | | +| value | 分段项的值 | string \| number | - | | +| icon | 分段项的显示图标 | ReactNode | - | | +| disabled | 分段项的禁用状态 | boolean | false | | +| className | 自定义类名 | string | - | | + +### CSS 变量 + +| 属性 | 说明 | 默认值 | +| --- | --- | --- | +| --background-color | 背景色 | `var(--adm-color-fill-content)` | +| --segment-selected-bg | 被选中分段的背景色 | `var(--adm-color-background)` | +| --segment-color | 分段项的文本颜色 | `var(--adm-color-text-secondary)` | +| --segment-selected-color | 被选中分段项的文本颜色 | `var(--adm-color-text)` | +| --segment-disabled-color | 禁用分段项的文本颜色 | `var(--adm-color-weak)` | diff --git a/src/components/segmented/segmented.less b/src/components/segmented/segmented.less new file mode 100644 index 0000000000..d2d59b04b2 --- /dev/null +++ b/src/components/segmented/segmented.less @@ -0,0 +1,116 @@ +@class-prefix-segmented: ~'adm-segmented'; + +.@{class-prefix-segmented} { + --background-color: var(--adm-color-fill-content); + --segment-selected-bg: var(--adm-color-background); + --segment-color: var(--adm-color-text-secondary); + --segment-selected-color: var(--adm-color-text); + --segment-disabled-color: var(--adm-color-weak); + --transition-time-function: cubic-bezier(0.645, 0.045, 0.355, 1); + + display: inline-block; + padding: 2px; + color: var(--segment-color); + background-color: var(--background-color); + border-radius: 2px; + transition: all 0.3s var(--transition-time-function); + + &-group { + position: relative; + display: flex; + align-items: stretch; + justify-items: flex-start; + width: 100%; + } + + // block styles + &&-block { + display: flex; + } + + &&-block &-item { + flex: 1; + min-width: 0; + } + + // item styles + &-item { + position: relative; + text-align: center; + cursor: pointer; + transition: color 0.3s var(--transition-time-function); + + &-selected { + .segmented-item-selected(); + color: var(--segment-selected-color); + } + + &-label { + min-height: 10px; + padding: 0 11px; + line-height: 28px; + .segmented-text-ellipsis(); + } + + // syntactic sugar to add `icon` for Segmented Item + &-icon + * { + margin-left: 6px; + } + + &-input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; + } + } + + // disabled styles + &-item-disabled { + .segmented-disabled-item(); + } + + // thumb styles + &-thumb { + .segmented-item-selected(); + + position: absolute; + top: 0; + left: 0; + width: 0; + height: 100%; + padding: 4px 0; + } + + // transition effect when `appear-active` + &-thumb-motion-appear-active { + transition: transform 0.3s var(--transition-time-function), + width 0.3s var(--transition-time-function); + will-change: transform, width; + } +} + +/* ---- mixins part starts ---- */ +.segmented-disabled-item { + color: var(--segment-disabled-color); + cursor: not-allowed; +} + +.segmented-item-selected { + background-color: var(--segment-selected-bg); + border-radius: 2px; + box-shadow: 0 2px 8px -2px fade(#000, 5%), 0 1px 4px -1px fade(#000, 7%), + 0 0 1px 0 fade(#000, 8%); +} + +.segmented-text-ellipsis { + overflow: hidden; + // handle text ellipsis + white-space: nowrap; + text-overflow: ellipsis; + word-break: keep-all; +} +/* ---- mixins part ends ---- */ diff --git a/src/components/segmented/segmented.tsx b/src/components/segmented/segmented.tsx new file mode 100644 index 0000000000..944050716b --- /dev/null +++ b/src/components/segmented/segmented.tsx @@ -0,0 +1,111 @@ +import classNames from 'classnames' +import type { + SegmentedLabeledOption as RcSegmentedLabeledOption, + SegmentedProps as RCSegmentedProps, + SegmentedRawOption, +} from 'rc-segmented' +import RcSegmented from 'rc-segmented' +import * as React from 'react' + +import { NativeProps, withNativeProps } from '../../utils/native-props' + +export type { SegmentedValue } from 'rc-segmented' + +interface SegmentedLabeledOptionWithoutIcon extends RcSegmentedLabeledOption { + label: RcSegmentedLabeledOption['label'] +} + +interface SegmentedLabeledOptionWithIcon + extends Omit { + label?: RcSegmentedLabeledOption['label'] + /** Set icon for Segmented item */ + icon: React.ReactNode +} + +function isSegmentedLabeledOptionWithIcon( + option: + | SegmentedRawOption + | SegmentedLabeledOptionWithIcon + | SegmentedLabeledOptionWithoutIcon +): option is SegmentedLabeledOptionWithIcon { + return ( + typeof option === 'object' && + !!(option as SegmentedLabeledOptionWithIcon)?.icon + ) +} + +export type SegmentedLabeledOption = + | SegmentedLabeledOptionWithIcon + | SegmentedLabeledOptionWithoutIcon + +interface InternalSegmentedProps + extends Omit { + options: (SegmentedRawOption | SegmentedLabeledOption)[] + /** Option to fit width to its parent's width */ + block?: boolean +} + +export type SegmentedProps = InternalSegmentedProps & + NativeProps< + | '--font-size' + | '--color' + | '--placeholder-color' + | '--disabled-color' + | '--text-align' + | '--caret-width' + | '--caret-color' + > + +const classPrefix = `adm-segmented` + +const Segmented = React.forwardRef( + (props, ref) => { + const { + prefixCls: customizePrefixCls, + className, + block, + options = [], + ...restProps + } = props + + // syntactic sugar to support `icon` for Segmented Item + const extendedOptions = React.useMemo( + () => + options.map(option => { + if (isSegmentedLabeledOptionWithIcon(option)) { + const { icon, label, ...restOption } = option + return { + ...restOption, + label: ( + <> + {icon} + {label && {label}} + + ), + } + } + return option + }), + [options, classPrefix] + ) + + return withNativeProps( + props, + + ) + } +) + +if (process.env.NODE_ENV !== 'production') { + Segmented.displayName = 'Segmented' +} + +export { Segmented } diff --git a/src/components/segmented/tests/__snapshots__/segmented.test.tsx.snap b/src/components/segmented/tests/__snapshots__/segmented.test.tsx.snap new file mode 100644 index 0000000000..1a126b960f --- /dev/null +++ b/src/components/segmented/tests/__snapshots__/segmented.test.tsx.snap @@ -0,0 +1,705 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`adm-segmented render empty segmented 1`] = ` +
+
+
+`; + +exports[`adm-segmented render label with ReactNode 1`] = ` +
+
+ + + +
+
+`; + +exports[`adm-segmented render segmented ok 1`] = ` +
+
+ + + +
+
+`; + +exports[`adm-segmented render segmented with \`block\` 1`] = ` +
+
+ + + +
+
+`; + +exports[`adm-segmented render segmented with mixed options 1`] = ` +
+
+ + + +
+
+`; + +exports[`adm-segmented render segmented with numeric options 1`] = ` +
+
+ + + + + +
+
+`; + +exports[`adm-segmented render segmented with options null/undefined 1`] = ` +
+
+