diff --git a/assets/index.less b/assets/index.less
index 801039c..e3589cf 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -1,16 +1,22 @@
@segmented-prefix-cls: rc-segmented;
+@disabled-color: fade(#000, 25%);
+@selected-bg-color: white;
+@text-color: #262626;
+@transition-duration: 0.3s;
+@transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
+
.segmented-disabled-item() {
&,
&:hover,
&:focus {
- color: fade(#000, 25%);
+ color: @disabled-color;
cursor: not-allowed;
}
}
.segmented-item-selected() {
- background-color: white;
+ background-color: @selected-bg-color;
}
.@{segmented-prefix-cls} {
@@ -21,9 +27,9 @@
&-group {
position: relative;
display: flex;
+ flex-direction: row;
align-items: stretch;
- justify-items: flex-start;
-
+ justify-content: flex-start;
width: 100%;
border-radius: 2px;
}
@@ -32,19 +38,18 @@
position: relative;
min-height: 28px;
padding: 4px 10px;
-
color: fade(#000, 85%);
text-align: center;
cursor: pointer;
&-selected {
.segmented-item-selected();
- color: #262626;
+ color: @text-color;
}
&:hover,
&:focus {
- color: #262626;
+ color: @text-color;
}
&-disabled {
@@ -60,7 +65,6 @@
position: absolute;
top: 0;
left: 0;
-
width: 0;
height: 0;
opacity: 0;
@@ -68,29 +72,44 @@
}
}
- // disabled styles
- &-disabled &-item,
- &-disabled &-item:hover,
- &-disabled &-item:focus {
- .segmented-disabled-item();
- }
-
&-thumb {
.segmented-item-selected();
-
position: absolute;
- // top: 0;
- // left: 0;
width: 0;
height: 100%;
padding: 4px 0;
+ transition: transform @transition-duration @transition-timing-function,
+ width @transition-duration @transition-timing-function;
+ }
+
+ &-vertical &-group {
+ flex-direction: column;
+ }
+
+ &-vertical &-item {
+ width: 100%;
+ text-align: left;
+ }
+
+ &-vertical &-thumb {
+ width: 100%;
+ height: 0;
+ padding: 0 4px;
+ transition: transform @transition-duration @transition-timing-function,
+ height @transition-duration @transition-timing-function;
+ }
+
+ // disabled styles
+ &-disabled &-item,
+ &-disabled &-item:hover,
+ &-disabled &-item:focus {
+ .segmented-disabled-item();
}
- // transition effect when `enter-active`
&-thumb-motion-appear-active,
&-thumb-motion-enter-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);
+ transition: transform @transition-duration @transition-timing-function,
+ width @transition-duration @transition-timing-function;
will-change: transform, width;
}
diff --git a/docs/demo/basic.tsx b/docs/demo/basic.tsx
index 649a853..9be5161 100644
--- a/docs/demo/basic.tsx
+++ b/docs/demo/basic.tsx
@@ -1,6 +1,6 @@
-import '../../assets/style.less';
-import React from 'react';
import Segmented from 'rc-segmented';
+import React from 'react';
+import '../../assets/style.less';
export default function App() {
return (
@@ -11,6 +11,13 @@ export default function App() {
onChange={(value) => console.log(value, typeof value)}
/>
+
+ console.log(value, typeof value)}
+ />
+
- targetElement
- ? {
- left: targetElement.offsetLeft,
- right:
- (targetElement.parentElement!.clientWidth as number) -
- targetElement.clientWidth -
- targetElement.offsetLeft,
- width: targetElement.clientWidth,
- }
- : null;
+ vertical?: boolean,
+): ThumbReact => {
+ if (!targetElement) return null;
+
+ const style: ThumbReact = {
+ left: targetElement.offsetLeft,
+ right:
+ (targetElement.parentElement!.clientWidth as number) -
+ targetElement.clientWidth -
+ targetElement.offsetLeft,
+ width: targetElement.clientWidth,
+ top: targetElement.offsetTop,
+ bottom:
+ (targetElement.parentElement!.clientHeight as number) -
+ targetElement.clientHeight -
+ targetElement.offsetTop,
+ height: targetElement.clientHeight,
+ };
+
+ if (vertical) {
+ // Adjusts positioning and size for vertical layout by setting horizontal properties to 0 and using vertical properties from the style object.
+ return {
+ left: 0,
+ right: 0,
+ width: 0,
+ top: style.top,
+ bottom: style.bottom,
+ height: style.height,
+ };
+ }
+
+ return {
+ left: style.left,
+ right: style.right,
+ width: style.width,
+ top: 0,
+ bottom: 0,
+ height: 0,
+ };
+};
-const toPX = (value: number) =>
+const toPX = (value: number | undefined): string | undefined =>
value !== undefined ? `${value}px` : undefined;
export default function MotionThumb(props: MotionThumbInterface) {
@@ -49,6 +82,7 @@ export default function MotionThumb(props: MotionThumbInterface) {
onMotionStart,
onMotionEnd,
direction,
+ vertical = false,
} = props;
const thumbRef = React.useRef(null);
@@ -57,11 +91,9 @@ export default function MotionThumb(props: MotionThumbInterface) {
// =========================== Effect ===========================
const findValueElement = (val: SegmentedValue) => {
const index = getValueIndex(val);
-
const ele = containerRef.current?.querySelectorAll(
`.${prefixCls}-item`,
)[index];
-
return ele?.offsetParent && ele;
};
@@ -73,8 +105,8 @@ export default function MotionThumb(props: MotionThumbInterface) {
const prev = findValueElement(prevValue);
const next = findValueElement(value);
- const calcPrevStyle = calcThumbStyle(prev);
- const calcNextStyle = calcThumbStyle(next);
+ const calcPrevStyle = calcThumbStyle(prev, vertical);
+ const calcNextStyle = calcThumbStyle(next, vertical);
setPrevValue(value);
setPrevStyle(calcPrevStyle);
@@ -88,34 +120,59 @@ export default function MotionThumb(props: MotionThumbInterface) {
}
}, [value]);
- const thumbStart = React.useMemo(
- () =>
- direction === 'rtl'
- ? toPX(-(prevStyle?.right as number))
- : toPX(prevStyle?.left as number),
- [direction, prevStyle],
- );
- const thumbActive = React.useMemo(
- () =>
- direction === 'rtl'
- ? toPX(-(nextStyle?.right as number))
- : toPX(nextStyle?.left as number),
- [direction, nextStyle],
- );
+ const thumbStart = React.useMemo(() => {
+ if (vertical) {
+ return toPX(prevStyle?.top ?? 0);
+ }
+
+ if (direction === 'rtl') {
+ return toPX(-(prevStyle?.right as number));
+ }
+
+ return toPX(prevStyle?.left as number);
+ }, [vertical, direction, prevStyle]);
+
+ const thumbActive = React.useMemo(() => {
+ if (vertical) {
+ return toPX(nextStyle?.top ?? 0);
+ }
+
+ if (direction === 'rtl') {
+ return toPX(-(nextStyle?.right as number));
+ }
+
+ return toPX(nextStyle?.left as number);
+ }, [vertical, direction, nextStyle]);
// =========================== Motion ===========================
const onAppearStart = () => {
+ if (vertical) {
+ return {
+ transform: 'translateY(var(--thumb-start-top))',
+ height: 'var(--thumb-start-height)',
+ };
+ }
+
return {
- transform: `translateX(var(--thumb-start-left))`,
- width: `var(--thumb-start-width)`,
+ transform: 'translateX(var(--thumb-start-left))',
+ width: 'var(--thumb-start-width)',
};
};
+
const onAppearActive = () => {
+ if (vertical) {
+ return {
+ transform: 'translateY(var(--thumb-active-top))',
+ height: 'var(--thumb-active-height)',
+ };
+ }
+
return {
- transform: `translateX(var(--thumb-active-left))`,
- width: `var(--thumb-active-width)`,
+ transform: 'translateX(var(--thumb-active-left))',
+ width: 'var(--thumb-active-width)',
};
};
+
const onVisibleChanged = () => {
setPrevStyle(null);
setNextStyle(null);
@@ -144,6 +201,10 @@ export default function MotionThumb(props: MotionThumbInterface) {
'--thumb-start-width': toPX(prevStyle?.width),
'--thumb-active-left': thumbActive,
'--thumb-active-width': toPX(nextStyle?.width),
+ '--thumb-start-top': thumbStart,
+ '--thumb-start-height': toPX(prevStyle?.height),
+ '--thumb-active-top': thumbActive,
+ '--thumb-active-height': toPX(nextStyle?.height),
} as React.CSSProperties;
// It's little ugly which should be refactor when @umi/test update to latest jsdom
diff --git a/src/index.tsx b/src/index.tsx
index 9c0fa27..3ef6f6c 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -39,6 +39,7 @@ export interface SegmentedProps
prefixCls?: string;
direction?: 'ltr' | 'rtl';
motionName?: string;
+ vertical?: boolean;
}
function getValidTitle(option: SegmentedLabeledOption) {
@@ -56,13 +57,11 @@ function normalizeOptions(options: SegmentedOptions): SegmentedLabeledOption[] {
return options.map((option) => {
if (typeof option === 'object' && option !== null) {
const validTitle = getValidTitle(option);
-
return {
...option,
title: validTitle,
};
}
-
return {
label: option?.toString(),
title: option?.toString(),
@@ -97,7 +96,6 @@ const InternalSegmentedOption: React.FC<{
if (disabled) {
return;
}
-
onChange(event, value);
};
@@ -131,6 +129,7 @@ const Segmented = React.forwardRef(
const {
prefixCls = 'rc-segmented',
direction,
+ vertical,
options = [],
disabled,
defaultValue,
@@ -168,9 +167,7 @@ const Segmented = React.forwardRef(
if (disabled) {
return;
}
-
setRawValue(val);
-
onChange?.(val);
};
@@ -186,6 +183,7 @@ const Segmented = React.forwardRef(
{
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-disabled`]: disabled,
+ [`${prefixCls}-vertical`]: vertical,
},
className,
)}
@@ -193,6 +191,7 @@ const Segmented = React.forwardRef(
>
`;
+
+exports[`rc-segmented should render vertical segmented 1`] = `
+
+`;
+
+exports[`rc-segmented should render vertical segmented and handle thumb animations correctly 1`] = `
+
+`;
diff --git a/tests/index.test.tsx b/tests/index.test.tsx
index 59fb1b1..9086368 100644
--- a/tests/index.test.tsx
+++ b/tests/index.test.tsx
@@ -555,4 +555,98 @@ describe('rc-segmented', () => {
offsetParentSpy.mockRestore();
});
+
+ it('should render vertical segmented', () => {
+ const { container, asFragment } = render(
+ ,
+ );
+
+ expect(asFragment().firstChild).toMatchSnapshot();
+ expect(container.querySelector('.rc-segmented')).toHaveClass(
+ 'rc-segmented-vertical',
+ );
+ expectMatchChecked(container, [true, false, false]);
+ });
+ it('should render vertical segmented and handle thumb animations correctly', () => {
+ const offsetParentSpy = jest
+ .spyOn(HTMLElement.prototype, 'offsetParent', 'get')
+ .mockImplementation(() => {
+ return container;
+ });
+ const handleValueChange = jest.fn();
+ const { container, asFragment } = render(
+ handleValueChange(value)}
+ />,
+ );
+
+ // Snapshot test
+ expect(asFragment().firstChild).toMatchSnapshot();
+ expect(container.querySelector('.rc-segmented')).toHaveClass(
+ 'rc-segmented-vertical',
+ );
+ expectMatchChecked(container, [true, false, false]);
+
+ // Click: Web
+ fireEvent.click(container.querySelectorAll('.rc-segmented-item-input')[2]);
+ expect(handleValueChange).toBeCalledWith('Web');
+ expectMatchChecked(container, [false, false, true]);
+
+ // Thumb should appear at `iOS`
+ exceptThumbHaveStyle(container, {
+ '--thumb-start-top': '0px',
+ '--thumb-start-height': '0px',
+ });
+
+ // Motion => active
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ // Motion enter end
+ fireEvent.animationEnd(container.querySelector('.rc-segmented-thumb')!);
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ // Thumb should disappear
+ expect(container.querySelector('.rc-segmented-thumb')).toBeFalsy();
+
+ // Click: Android
+ fireEvent.click(container.querySelectorAll('.rc-segmented-item-input')[1]);
+ expect(handleValueChange).toBeCalledWith('Android');
+ expectMatchChecked(container, [false, true, false]);
+
+ // Thumb should move
+ expect(container.querySelector('.rc-segmented-thumb')).toHaveClass(
+ 'rc-segmented-thumb-motion',
+ );
+
+ // Thumb appeared at `Web`
+ exceptThumbHaveStyle(container, {
+ '--thumb-start-top': '0px',
+ '--thumb-start-height': '0px',
+ });
+
+ // Motion appear end
+ act(() => {
+ jest.runAllTimers();
+ });
+ exceptThumbHaveStyle(container, {
+ '--thumb-active-top': '0px',
+ '--thumb-active-height': '0px',
+ });
+
+ fireEvent.animationEnd(container.querySelector('.rc-segmented-thumb')!);
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ // Thumb should disappear
+ expect(container.querySelector('.rc-segmented-thumb')).toBeFalsy();
+
+ offsetParentSpy.mockRestore();
+ });
});