Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/demo/basic.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '../../assets/style.less';
import React from 'react';
import Segmented from 'rc-segmented';

import React from 'react';
import '../../assets/style.less';
type Options = 'iOS' | 'Android' | 'Web';
export default function App() {
return (
<div>
Expand All @@ -21,7 +21,7 @@ export default function App() {
<Segmented options={['iOS', 'Android', 'Web']} disabled />
</div>
<div className="wrapper">
<Segmented
<Segmented<Options>
options={[
'iOS',
{ label: 'Android', value: 'Android', disabled: true },
Expand Down
14 changes: 8 additions & 6 deletions src/MotionThumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import CSSMotion from 'rc-motion';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import { composeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import type { SegmentedValue } from '.';
import type { SegmentedRawValue, SegmentedValue } from '.';

type ThumbReact = {
left: number;
right: number;
width: number;
} | null;

export interface MotionThumbInterface {
export interface MotionThumbInterface<T extends SegmentedRawValue> {
containerRef: React.RefObject<HTMLDivElement>;
value: SegmentedValue;
getValueIndex: (value: SegmentedValue) => number;
value: SegmentedValue<T>;
getValueIndex: (value: SegmentedValue<T>) => number;
prefixCls: string;
motionName: string;
onMotionStart: VoidFunction;
Expand All @@ -39,7 +39,9 @@ const calcThumbStyle = (
const toPX = (value: number) =>
value !== undefined ? `${value}px` : undefined;

export default function MotionThumb(props: MotionThumbInterface) {
export default function MotionThumb<T extends SegmentedRawValue>(
props: MotionThumbInterface<T>,
) {
const {
prefixCls,
containerRef,
Expand All @@ -55,7 +57,7 @@ export default function MotionThumb(props: MotionThumbInterface) {
const [prevValue, setPrevValue] = React.useState(value);

// =========================== Effect ===========================
const findValueElement = (val: SegmentedValue) => {
const findValueElement = (val: SegmentedValue<T>) => {
const index = getValueIndex(val);

const ele = containerRef.current?.querySelectorAll<HTMLDivElement>(
Expand Down
86 changes: 54 additions & 32 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,62 @@
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 SegmentedRawValue = string | number;

export type SegmentedRawOption = SegmentedValue;
export type SegmentedValue<T extends SegmentedRawValue = SegmentedRawValue> = T;

export interface SegmentedLabeledOption {
export type SegmentedRawOption<T extends SegmentedRawValue> = SegmentedValue<T>;

export interface SegmentedLabeledOption<T extends SegmentedRawValue> {
className?: string;
disabled?: boolean;
label: React.ReactNode;
value: SegmentedRawOption;
value: SegmentedRawOption<T>;
/**
* html `title` property for label
*/
title?: string;
}

type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[];
type SegmentedOptions<T extends SegmentedRawValue> = (
| SegmentedRawOption<T>
| SegmentedLabeledOption<T>
)[];

export interface SegmentedProps
type InternalSegmentedOptionProps<T extends SegmentedRawValue> = {
prefixCls: string;
className?: string;
disabled?: boolean;
checked: boolean;
label: React.ReactNode;
title?: string;
value: SegmentedRawOption<T>;
onChange: (
e: React.ChangeEvent<HTMLInputElement>,
value: SegmentedRawOption<T>,
) => void;
};

export interface SegmentedProps<T extends SegmentedRawValue>
extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange'> {
options: SegmentedOptions;
defaultValue?: SegmentedValue;
value?: SegmentedValue;
onChange?: (value: SegmentedValue) => void;
options: SegmentedOptions<T>;
defaultValue?: SegmentedValue<T>;
value?: SegmentedValue<T>;
onChange?: (value: SegmentedValue<T>) => void;
disabled?: boolean;
prefixCls?: string;
direction?: 'ltr' | 'rtl';
motionName?: string;
}

function getValidTitle(option: SegmentedLabeledOption) {
function getValidTitle<T extends SegmentedRawValue>(
option: SegmentedLabeledOption<T>,
) {
if (typeof option.title !== 'undefined') {
return option.title;
}
Expand All @@ -46,7 +67,9 @@ function getValidTitle(option: SegmentedLabeledOption) {
}
}

function normalizeOptions(options: SegmentedOptions): SegmentedLabeledOption[] {
function normalizeOptions<T extends SegmentedRawValue>(
options: SegmentedOptions<T>,
): SegmentedLabeledOption<T>[] {
return options.map((option) => {
if (typeof option === 'object' && option !== null) {
const validTitle = getValidTitle(option);
Expand All @@ -65,19 +88,9 @@ function normalizeOptions(options: SegmentedOptions): SegmentedLabeledOption[] {
});
}

const InternalSegmentedOption: React.FC<{
prefixCls: string;
className?: string;
disabled?: boolean;
checked: boolean;
label: React.ReactNode;
title?: string;
value: SegmentedRawOption;
onChange: (
e: React.ChangeEvent<HTMLInputElement>,
value: SegmentedRawOption,
) => void;
}> = ({
const InternalSegmentedOption: React.FC<InternalSegmentedOptionProps<any>> = <
T extends SegmentedRawValue,
>({
prefixCls,
className,
disabled,
Expand All @@ -86,7 +99,7 @@ const InternalSegmentedOption: React.FC<{
title,
value,
onChange,
}) => {
}: InternalSegmentedOptionProps<T>) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
return;
Expand Down Expand Up @@ -115,8 +128,11 @@ const InternalSegmentedOption: React.FC<{
);
};

const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
(props, ref) => {
const Segmented = React.forwardRef(
<T extends SegmentedRawValue>(
props: React.PropsWithChildren<SegmentedProps<T>>,
ref: React.Ref<HTMLDivElement>,
) => {
const {
prefixCls = 'rc-segmented',
direction,
Expand Down Expand Up @@ -152,7 +168,7 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(

const handleChange = (
event: React.ChangeEvent<HTMLInputElement>,
val: SegmentedRawOption,
val: SegmentedRawOption<any>,
) => {
if (disabled) {
return;
Expand Down Expand Up @@ -225,4 +241,10 @@ Segmented.defaultProps = {
options: [],
};

export default Segmented;
const TypeSegmented = Segmented as unknown as <T extends SegmentedRawValue>(
props: React.PropsWithChildren<SegmentedProps<T>> & {
ref?: React.Ref<HTMLDivElement>;
},
) => React.ReactElement;

export default TypeSegmented;
11 changes: 6 additions & 5 deletions tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { act, fireEvent, render } from '@testing-library/react';
import * as React from 'react';
import Segmented from '../src';

type Options = 'iOS' | 'Android' | 'Web';
jest.mock('rc-motion/lib/util/motion', () => {
return {
...jest.requireActual('rc-motion/lib/util/motion'),
Expand Down Expand Up @@ -47,7 +48,7 @@ describe('rc-segmented', () => {

it('render segmented ok', () => {
const { container, asFragment } = render(
<Segmented
<Segmented<Options>
options={[{ label: 'iOS', value: 'iOS' }, 'Android', 'Web']}
/>,
);
Expand Down Expand Up @@ -124,7 +125,7 @@ describe('rc-segmented', () => {
it('render segmented with options: 2', () => {
const handleValueChange = jest.fn();
const { container, asFragment } = render(
<Segmented
<Segmented<Options>
options={['iOS', { label: 'Android', value: 'Android' }, 'Web']}
onChange={(value) => handleValueChange(value)}
/>,
Expand All @@ -140,7 +141,7 @@ describe('rc-segmented', () => {
it('render segmented with options: disabled', () => {
const handleValueChange = jest.fn();
const { container, asFragment } = render(
<Segmented
<Segmented<Options>
options={[
'iOS',
{ label: 'Android', value: 'Android', disabled: true },
Expand Down Expand Up @@ -474,7 +475,7 @@ describe('rc-segmented', () => {

it('render segmented with title', () => {
const { asFragment, container } = render(
<Segmented
<Segmented<'Web' | 'hello2' | 'test2' | 'hello1' | 'foo2'>
options={[
'Web',
{
Expand Down Expand Up @@ -511,7 +512,7 @@ describe('rc-segmented', () => {

it('render with rtl', () => {
const { container } = render(
<Segmented
<Segmented<Options>
direction="rtl"
options={[{ label: 'iOS', value: 'iOS' }, 'Android', 'Web']}
/>,
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
"rc-segmented": ["src/index.tsx"]
}
},
"include": [".dumi/**/*", ".dumirc.ts", "src", "tests", "docs/examples"],
"include": [".dumi/**/*", ".dumirc.ts", "src", "tests", "docs/**/*"]
}