From d2ee55cc4fd6cdc6d2dbb79745c040eda2453432 Mon Sep 17 00:00:00 2001 From: wuxh Date: Thu, 7 Dec 2023 22:12:56 +0800 Subject: [PATCH 1/6] feat(ts): value supports generic definition --- src/index.tsx | 247 ++++++++++++++++++++++++++------------------------ 1 file changed, 129 insertions(+), 118 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index efb923c..a20dc25 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,8 @@ -import * as React from 'react'; import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import { composeRef } from 'rc-util/lib/ref'; import omit from 'rc-util/lib/omit'; +import { composeRef } from 'rc-util/lib/ref'; +import * as React from 'react'; import MotionThumb from './MotionThumb'; @@ -23,12 +23,15 @@ export interface SegmentedLabeledOption { type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[]; -export interface SegmentedProps +/** + * Generic parameter `Value` is supported since 2.3.0 + */ +export interface SegmentedProps extends Omit, 'onChange'> { options: SegmentedOptions; - defaultValue?: SegmentedValue; - value?: SegmentedValue; - onChange?: (value: SegmentedValue) => void; + defaultValue?: Value; + value?: Value; + onChange?: (value: Value) => void; disabled?: boolean; prefixCls?: string; direction?: 'ltr' | 'rtl'; @@ -65,7 +68,7 @@ function normalizeOptions(options: SegmentedOptions): SegmentedLabeledOption[] { }); } -const InternalSegmentedOption: React.FC<{ +interface _InternalSegmentedOptionProps { prefixCls: string; className?: string; disabled?: boolean; @@ -77,16 +80,20 @@ const InternalSegmentedOption: React.FC<{ e: React.ChangeEvent, value: SegmentedRawOption, ) => void; -}> = ({ - prefixCls, - className, - disabled, - checked, - label, - title, - value, - onChange, -}) => { +} + +const InternalSegmentedOption = (props: _InternalSegmentedOptionProps) => { + const { + prefixCls, + className, + disabled, + checked, + label, + title, + value, + onChange, + } = props; + const handleChange = (event: React.ChangeEvent) => { if (disabled) { return; @@ -115,110 +122,114 @@ const InternalSegmentedOption: React.FC<{ ); }; -const Segmented = React.forwardRef( - (props, ref) => { - const { - prefixCls = 'rc-segmented', - direction, - options = [], - disabled, - defaultValue, - value, - onChange, - className = '', - motionName = 'thumb-motion', - ...restProps - } = props; - - const containerRef = React.useRef(null); - const mergedRef = React.useMemo( - () => composeRef(containerRef, ref), - [containerRef, ref], - ); - - const segmentedOptions = React.useMemo(() => { - return normalizeOptions(options); - }, [options]); - - // Note: We should not auto switch value when value not exist in options - // which may break single source of truth. - const [rawValue, setRawValue] = useMergedState(segmentedOptions[0]?.value, { - value, - defaultValue, - }); - - // ======================= Change ======================== - const [thumbShow, setThumbShow] = React.useState(false); - - const handleChange = ( - event: React.ChangeEvent, - val: SegmentedRawOption, - ) => { - if (disabled) { - return; - } - - setRawValue(val); - - onChange?.(val); - }; +const Segmented = ( + props: SegmentedProps, + ref: React.ForwardedRef, +) => { + const { + prefixCls = 'rc-segmented', + direction, + options = [], + disabled, + defaultValue, + value, + onChange, + className = '', + motionName = 'thumb-motion', + ...restProps + } = props; + + const containerRef = React.useRef(null); + const mergedRef = React.useMemo( + () => composeRef(containerRef, ref), + [containerRef, ref], + ); + + const segmentedOptions = React.useMemo(() => { + return normalizeOptions(options); + }, [options]); + + // Note: We should not auto switch value when value not exist in options + // which may break single source of truth. + const [rawValue, setRawValue] = useMergedState(segmentedOptions[0]?.value, { + value, + defaultValue, + }); + + // ======================= Change ======================== + const [thumbShow, setThumbShow] = React.useState(false); - const divProps = omit(restProps, ['children']); - - return ( -
-
- , + val: SegmentedRawOption, + ) => { + if (disabled) { + return; + } + + setRawValue(val); + + onChange?.(val); + }; + + const divProps = omit(restProps, ['children']); + + return ( +
+
+ + segmentedOptions.findIndex((n) => n.value === val) + } + onMotionStart={() => { + setThumbShow(true); + }} + onMotionEnd={() => { + setThumbShow(false); + }} + /> + {segmentedOptions.map((segmentedOption) => ( + - segmentedOptions.findIndex((n) => n.value === val) - } - onMotionStart={() => { - setThumbShow(true); - }} - onMotionEnd={() => { - setThumbShow(false); - }} + className={classNames( + segmentedOption.className, + `${prefixCls}-item`, + { + [`${prefixCls}-item-selected`]: + segmentedOption.value === rawValue && !thumbShow, + }, + )} + checked={segmentedOption.value === rawValue} + onChange={handleChange} + disabled={!!disabled || !!segmentedOption.disabled} /> - {segmentedOptions.map((segmentedOption) => ( - - ))} -
+ ))}
- ); - }, -); +
+ ); +}; -Segmented.displayName = 'Segmented'; +const RefSegmented = React.forwardRef(Segmented); +RefSegmented.displayName = 'Segmented'; -export default Segmented; +export default RefSegmented as ( + props: SegmentedProps & { ref?: React.Ref }, +) => React.ReactElement; From 792c6dc69e48f2c9785ac5e968bfcbe724792dda Mon Sep 17 00:00:00 2001 From: wuxh Date: Thu, 7 Dec 2023 22:13:32 +0800 Subject: [PATCH 2/6] docs: update demo --- docs/demo/basic.tsx | 15 ++++++++++++--- tsconfig.json | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/demo/basic.tsx b/docs/demo/basic.tsx index 649a853..a71fbdb 100644 --- a/docs/demo/basic.tsx +++ b/docs/demo/basic.tsx @@ -1,14 +1,23 @@ -import '../../assets/style.less'; -import React from 'react'; import Segmented from 'rc-segmented'; +import React from 'react'; +import '../../assets/style.less'; + +enum Platform { + iOS = 'iOS', + Android = 'Android', + Web = 'Web', +} export default function App() { + const [platform, setPlatform] = React.useState(Platform.iOS); + return (
console.log(value, typeof value)} + onChange={setPlatform} />
diff --git a/tsconfig.json b/tsconfig.json index 850f783..eddc287 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,5 @@ "rc-segmented": ["src/index.tsx"] } }, - "include": [".dumi/**/*", ".dumirc.ts", "src", "tests", "docs/examples"], + "include": [".dumi/**/*", ".dumirc.ts", "src", "tests", "docs/demo"] } From a4809cb5e4d821142d83be23d656da0a102ccdc7 Mon Sep 17 00:00:00 2001 From: wuxh Date: Thu, 7 Dec 2023 22:37:44 +0800 Subject: [PATCH 3/6] fix: pins types-ramda to 0.29.4 ref: https://github.com/ramda/ramda/issues/3415 --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 057fdb5..6cfc25d 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,9 @@ "react": ">=16.0.0", "react-dom": ">=16.0.0" }, + "resolutions": { + "types-ramda": "0.29.4" + }, "cnpm": { "mode": "npm" }, From 50a01a9ca7db9179b31490a3950f3f86dd4be914 Mon Sep 17 00:00:00 2001 From: wuxh Date: Fri, 8 Dec 2023 10:37:15 +0800 Subject: [PATCH 4/6] chore: rename --- src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index a20dc25..5e0c403 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -68,7 +68,7 @@ function normalizeOptions(options: SegmentedOptions): SegmentedLabeledOption[] { }); } -interface _InternalSegmentedOptionProps { +interface InternalSegmentedOptionProps { prefixCls: string; className?: string; disabled?: boolean; @@ -82,7 +82,7 @@ interface _InternalSegmentedOptionProps { ) => void; } -const InternalSegmentedOption = (props: _InternalSegmentedOptionProps) => { +const InternalSegmentedOption = (props: InternalSegmentedOptionProps) => { const { prefixCls, className, From 6fd3169aaa09ef237275808e5d3f8a628836ad19 Mon Sep 17 00:00:00 2001 From: wuxh Date: Fri, 8 Dec 2023 11:19:02 +0800 Subject: [PATCH 5/6] feat: options Value support generic --- src/index.tsx | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 5e0c403..f92d3b0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,25 +10,32 @@ export type SegmentedValue = string | number; export type SegmentedRawOption = SegmentedValue; -export interface SegmentedLabeledOption { +export interface SegmentedLabeledOption< + Value extends SegmentedRawOption = SegmentedRawOption, +> { className?: string; disabled?: boolean; label: React.ReactNode; - value: SegmentedRawOption; + value: Value; /** * html `title` property for label */ title?: string; } -type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[]; +type SegmentedOptions< + PlainValue extends SegmentedRawOption = SegmentedRawOption, + Value extends SegmentedRawOption = PlainValue, +> = (PlainValue | SegmentedLabeledOption)[]; /** * Generic parameter `Value` is supported since 2.3.0 */ -export interface SegmentedProps - extends Omit, 'onChange'> { - options: SegmentedOptions; +export interface SegmentedProps< + PlainValue extends SegmentedRawOption = SegmentedRawOption, + Value extends SegmentedRawOption = PlainValue, +> extends Omit, 'onChange'> { + options: SegmentedOptions; defaultValue?: Value; value?: Value; onChange?: (value: Value) => void; @@ -49,8 +56,10 @@ function getValidTitle(option: SegmentedLabeledOption) { } } -function normalizeOptions(options: SegmentedOptions): SegmentedLabeledOption[] { - return options.map((option) => { +function normalizeOptions< + Value extends SegmentedRawOption = SegmentedRawOption, +>(options: SegmentedOptions) { + return options.map>((option) => { if (typeof option === 'object' && option !== null) { const validTitle = getValidTitle(option); @@ -230,6 +239,11 @@ const Segmented = ( const RefSegmented = React.forwardRef(Segmented); RefSegmented.displayName = 'Segmented'; -export default RefSegmented as ( - props: SegmentedProps & { ref?: React.Ref }, +export default RefSegmented as < + PlainValue extends SegmentedRawOption = SegmentedRawOption, + Value extends SegmentedRawOption = PlainValue, +>( + props: SegmentedProps & { + ref?: React.Ref; + }, ) => React.ReactElement; From 6e8022a741592eca17370a5d899ddbec249c5439 Mon Sep 17 00:00:00 2001 From: wuxh Date: Fri, 8 Dec 2023 11:19:11 +0800 Subject: [PATCH 6/6] chore: update demo --- docs/demo/basic.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/demo/basic.tsx b/docs/demo/basic.tsx index a71fbdb..18f6821 100644 --- a/docs/demo/basic.tsx +++ b/docs/demo/basic.tsx @@ -16,7 +16,7 @@ export default function App() {
@@ -30,11 +30,19 @@ export default function App() {
- + options={[ + Platform.iOS, + { label: 'Android', value: Platform.Android, disabled: true }, + Platform.Web, + ]} + /> +
+ options={[ - 'iOS', - { label: 'Android', value: 'Android', disabled: true }, - 'Web', + Platform.iOS, + { label: 'Android', value: 'Linux', disabled: true }, + Platform.Web, ]} />