diff --git a/docs/demo/basic.tsx b/docs/demo/basic.tsx index 649a853..49b5caf 100644 --- a/docs/demo/basic.tsx +++ b/docs/demo/basic.tsx @@ -1,8 +1,9 @@ -import '../../assets/style.less'; -import React from 'react'; import Segmented from 'rc-segmented'; +import React from 'react'; -export default function App() { +import '../../assets/style.less'; + +const Demo = () => { return (
@@ -27,8 +28,11 @@ export default function App() { { label: 'Android', value: 'Android', disabled: true }, 'Web', ]} + onChange={(value) => console.log(value, typeof value)} />
); -} +}; + +export default Demo; diff --git a/docs/demo/controlled.tsx b/docs/demo/controlled.tsx index 2546819..d032844 100644 --- a/docs/demo/controlled.tsx +++ b/docs/demo/controlled.tsx @@ -1,15 +1,9 @@ -import '../../assets/style.less'; -import React from 'react'; import Segmented from 'rc-segmented'; -import type { SegmentedValue } from 'rc-segmented'; +import React from 'react'; +import '../../assets/style.less'; -export default class Demo extends React.Component< - unknown, - { value: SegmentedValue } -> { - state = { - value: 'Web3', - }; +export default class Demo extends React.Component { + state = { value: 'Web3' }; render() { return ( diff --git a/docs/demo/refs.tsx b/docs/demo/refs.tsx index 5a7e345..080f7b6 100644 --- a/docs/demo/refs.tsx +++ b/docs/demo/refs.tsx @@ -1,11 +1,11 @@ -import '../../assets/style.less'; -import React from 'react'; import Segmented from 'rc-segmented'; +import React from 'react'; +import '../../assets/style.less'; class ClassComponentWithStringRef extends React.Component { componentDidMount() { // eslint-disable-next-line react/no-string-refs - console.log(this.refs.segmentedRef, 'ref'); + console.log(this.refs.segmentedRef, 'ClassComponentWithStringRef'); } render() { @@ -22,7 +22,7 @@ class ClassComponent2 extends React.Component { segmentedRef: HTMLDivElement | null = null; componentDidMount() { - console.log(this.segmentedRef, 'ref'); + console.log(this.segmentedRef, 'ClassComponent2'); } render() { @@ -42,7 +42,7 @@ class ClassComponentWithCreateRef extends React.Component< segmentedRef = React.createRef(); componentDidMount() { - console.log(this.segmentedRef.current, 'ref'); + console.log(this.segmentedRef.current, 'ClassComponentWithCreateRef'); } render() { @@ -55,7 +55,7 @@ class ClassComponentWithCreateRef extends React.Component< function FunctionalComponent() { const segmentedRef = React.useRef(null); React.useEffect(() => { - console.log(segmentedRef.current, 'ref'); + console.log(segmentedRef.current, 'FunctionalComponent'); }, []); return ; } diff --git a/package.json b/package.json index b0a639a..4971169 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "ts-node": "^10.9.1", - "typescript": "^4.9.4" + "typescript": "^5.3.3" }, "peerDependencies": { "react": ">=16.0.0", diff --git a/src/MotionThumb.tsx b/src/MotionThumb.tsx index a2e26cc..32584f5 100644 --- a/src/MotionThumb.tsx +++ b/src/MotionThumb.tsx @@ -11,10 +11,10 @@ type ThumbReact = { width: number; } | null; -export interface MotionThumbInterface { +export interface MotionThumbInterface { containerRef: React.RefObject; - value: SegmentedValue; - getValueIndex: (value: SegmentedValue) => number; + value: Value; + getValueIndex: (value: Value) => number; prefixCls: string; motionName: string; onMotionStart: VoidFunction; @@ -55,7 +55,7 @@ export default function MotionThumb(props: MotionThumbInterface) { const [prevValue, setPrevValue] = React.useState(value); // =========================== Effect =========================== - const findValueElement = (val: SegmentedValue) => { + const findValueElement = (val: any) => { const index = getValueIndex(val); const ele = containerRef.current?.querySelectorAll( diff --git a/src/index.tsx b/src/index.tsx index efb923c..76e3e75 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,34 +1,38 @@ -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'; -export type SegmentedValue = string | number; - -export type SegmentedRawOption = SegmentedValue; +export type SegmentedValue = string | number | boolean; -export interface SegmentedLabeledOption { +export interface SegmentedLabeledOption { className?: string; disabled?: boolean; label: React.ReactNode; - value: SegmentedRawOption; + value: Value; /** * html `title` property for label */ title?: string; } -type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[]; - -export interface SegmentedProps - extends Omit, 'onChange'> { - options: SegmentedOptions; - defaultValue?: SegmentedValue; - value?: SegmentedValue; - onChange?: (value: SegmentedValue) => void; +type SegmentedOptions = ( + | Value + | SegmentedLabeledOption +)[]; + +export interface SegmentedProps + extends Omit< + React.HTMLProps, + 'value' | 'defaultValue' | 'onChange' + > { + options?: SegmentedOptions; + defaultValue?: Value; + value?: Value; + onChange?: (value: Value) => void; disabled?: boolean; prefixCls?: string; direction?: 'ltr' | 'rtl'; @@ -72,11 +76,8 @@ const InternalSegmentedOption: React.FC<{ checked: boolean; label: React.ReactNode; title?: string; - value: SegmentedRawOption; - onChange: ( - e: React.ChangeEvent, - value: SegmentedRawOption, - ) => void; + value: any; + onChange: (e: React.ChangeEvent, value: any) => void; }> = ({ prefixCls, className, @@ -115,110 +116,115 @@ 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 InternalSegmented: React.ForwardRefRenderFunction< + HTMLDivElement, + SegmentedProps +> = (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, + }); - const divProps = omit(restProps, ['children']); - - return ( -
-
- , + val: any, + ) => { + 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 Segmented = React.forwardRef( + InternalSegmented, +) as (( + props: SegmentedProps & { ref?: React.Ref }, +) => React.ReactElement) & { displayName?: string }; + +if (process.env.NODE_ENV !== 'production') { + Segmented.displayName = 'Segmented'; +} export default Segmented; diff --git a/tests/__snapshots__/index.test.tsx.snap b/tests/__snapshots__/index.test.tsx.snap index fb6277d..327991d 100644 --- a/tests/__snapshots__/index.test.tsx.snap +++ b/tests/__snapshots__/index.test.tsx.snap @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`rc-segmented render empty options segmented 1`] = ` +
+
+
+`; + exports[`rc-segmented render empty segmented 1`] = `
{ expect(asFragment().firstChild).toMatchSnapshot(); }); + it('render empty options segmented', () => { + const { asFragment } = render(); + expect(asFragment().firstChild).toMatchSnapshot(); + }); + it('render segmented ok', () => { const { container, asFragment } = render( { }); it('render segmented with controlled mode', () => { - const offsetParentSpy = jest - .spyOn(HTMLElement.prototype, 'offsetParent', 'get') - .mockImplementation(() => { - return container; - }); - const Demo = () => { const options = ['iOS', 'Android', 'Web3']; @@ -257,6 +256,12 @@ describe('rc-segmented', () => { }; const { container } = render(); + const offsetParentSpy = jest + .spyOn(HTMLElement.prototype, 'offsetParent', 'get') + .mockImplementation(() => { + return container; + }); + fireEvent.click(container.querySelectorAll('.rc-segmented-item-input')[0]); expect(container.querySelector('.value')?.textContent).toBe('iOS'); @@ -297,11 +302,6 @@ describe('rc-segmented', () => { describe('render segmented with CSSMotion', () => { it('basic', () => { - const offsetParentSpy = jest - .spyOn(HTMLElement.prototype, 'offsetParent', 'get') - .mockImplementation(() => { - return container; - }); const handleValueChange = jest.fn(); const { container, asFragment } = render( { onChange={(value) => handleValueChange(value)} />, ); + + const offsetParentSpy = jest + .spyOn(HTMLElement.prototype, 'offsetParent', 'get') + .mockImplementation(() => { + return container; + }); + expect(asFragment().firstChild).toMatchSnapshot(); expectMatchChecked(container, [true, false, false]); @@ -385,11 +392,6 @@ describe('rc-segmented', () => { }); it('quick switch', () => { - const offsetParentSpy = jest - .spyOn(HTMLElement.prototype, 'offsetParent', 'get') - .mockImplementation(() => { - return container; - }); const { container } = render( { />, ); + const offsetParentSpy = jest + .spyOn(HTMLElement.prototype, 'offsetParent', 'get') + .mockImplementation(() => { + return container; + }); + // >>> Click: Web3 fireEvent.click( container.querySelectorAll('.rc-segmented-item-input')[2], @@ -525,11 +533,6 @@ describe('rc-segmented', () => { }); it('click can work as expected with rtl', () => { - const offsetParentSpy = jest - .spyOn(HTMLElement.prototype, 'offsetParent', 'get') - .mockImplementation(() => { - return container; - }); const handleValueChange = jest.fn(); const { container } = render( { />, ); + const offsetParentSpy = jest + .spyOn(HTMLElement.prototype, 'offsetParent', 'get') + .mockImplementation(() => { + return container; + }); + fireEvent.click(container.querySelectorAll('.rc-segmented-item-input')[1]); expectMatchChecked(container, [false, true, false]); expect(handleValueChange).toBeCalledWith('Android'); diff --git a/tsconfig.json b/tsconfig.json index 850f783..4681e24 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": [".dumirc.ts", "**/*"] }