Skip to content

Commit cb2be8c

Browse files
authored
fix: trigger thumb animation when selected changed (#31)
1 parent a9f646c commit cb2be8c

File tree

3 files changed

+109
-31
lines changed

3 files changed

+109
-31
lines changed

docs/examples/controlled.tsx

+21-9
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,27 @@ export default class Demo extends React.Component<
1313

1414
render() {
1515
return (
16-
<Segmented
17-
options={['iOS', 'Android', 'Web3']}
18-
value={this.state.value}
19-
onChange={(e) =>
20-
this.setState({
21-
value: e.target.value,
22-
})
23-
}
24-
/>
16+
<>
17+
<Segmented
18+
options={['iOS', 'Android', 'Web3']}
19+
value={this.state.value}
20+
onChange={(e) =>
21+
this.setState({
22+
value: e.target.value,
23+
})
24+
}
25+
/>
26+
&nbsp;&nbsp;
27+
<Segmented
28+
options={['iOS', 'Android', 'Web3']}
29+
value={this.state.value}
30+
onChange={(e) =>
31+
this.setState({
32+
value: e.target.value,
33+
})
34+
}
35+
/>
36+
</>
2537
);
2638
}
2739
}

src/index.tsx

+59-21
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const InternalSegmentedOption: React.FC<{
104104

105105
return (
106106
<label
107-
className={classNames(`${prefixCls}-item`, className, {
107+
className={classNames(className, {
108108
[`${prefixCls}-item-disabled`]: disabled,
109109
})}
110110
>
@@ -155,7 +155,7 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
155155
}, [options]);
156156

157157
const [selected, setSelected] = useMergedState(segmentedOptions[0]?.value, {
158-
value,
158+
value: props.value,
159159
defaultValue,
160160
});
161161

@@ -165,24 +165,60 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
165165

166166
const [thumbShow, setThumbShow] = React.useState(false);
167167

168-
const calcThumbMoveStatus = (
169-
event: React.ChangeEvent<HTMLInputElement>,
170-
) => {
171-
const toElement = event.target.closest(`.${prefixCls}-item`);
168+
const doThumbAnimation = React.useCallback(
169+
(selectedValue: SegmentedRawOption) => {
170+
const segmentedItemIndex = segmentedOptions.findIndex(
171+
(n) => n.value === selectedValue,
172+
);
172173

173-
const fromElement = containerRef.current?.querySelector(
174-
`.${prefixCls}-item-selected`,
175-
);
174+
if (segmentedItemIndex < 0) {
175+
return;
176+
}
176177

177-
if (fromElement && toElement && thumbMoveStatus.current) {
178-
thumbMoveStatus.current.from = calcThumbStyle(
179-
fromElement as HTMLElement,
178+
// find target element
179+
const toElement = containerRef.current?.querySelector(
180+
`.${prefixCls}-item:nth-child(${segmentedItemIndex + 1})`,
180181
);
181-
thumbMoveStatus.current.to = calcThumbStyle(toElement as HTMLElement);
182182

183-
setThumbShow(true);
183+
if (toElement) {
184+
// find source element
185+
const fromElement = containerRef.current?.querySelector(
186+
`.${prefixCls}-item-selected`,
187+
);
188+
189+
if (fromElement && toElement && thumbMoveStatus.current) {
190+
// calculate for thumb moving animation
191+
thumbMoveStatus.current.from = calcThumbStyle(
192+
fromElement as HTMLElement,
193+
);
194+
thumbMoveStatus.current.to = calcThumbStyle(
195+
toElement as HTMLElement,
196+
);
197+
198+
// trigger css-motion starts
199+
setThumbShow(true);
200+
}
201+
}
202+
},
203+
[prefixCls, segmentedOptions],
204+
);
205+
206+
// get latest version of `visualSelected`
207+
const latestVisualSelected = React.useRef(visualSelected);
208+
React.useEffect(() => {
209+
latestVisualSelected.current = visualSelected;
210+
});
211+
212+
React.useEffect(() => {
213+
// Syncing `visualSelected` when `selected` changed
214+
// and do thumb animation
215+
if (
216+
(typeof selected === 'string' || typeof selected === 'number') &&
217+
selected !== latestVisualSelected.current
218+
) {
219+
doThumbAnimation(selected);
184220
}
185-
};
221+
}, [selected]);
186222

187223
const handleChange = (
188224
event: React.ChangeEvent<HTMLInputElement>,
@@ -192,8 +228,6 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
192228
return;
193229
}
194230

195-
calcThumbMoveStatus(event);
196-
197231
setSelected(val);
198232

199233
if (onChange) {
@@ -275,10 +309,14 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
275309
<InternalSegmentedOption
276310
key={segmentedOption.value}
277311
prefixCls={prefixCls}
278-
className={classNames(segmentedOption.className, {
279-
[`${prefixCls}-item-selected`]:
280-
segmentedOption.value === visualSelected,
281-
})}
312+
className={classNames(
313+
segmentedOption.className,
314+
`${prefixCls}-item`,
315+
{
316+
[`${prefixCls}-item-selected`]:
317+
segmentedOption.value === visualSelected,
318+
},
319+
)}
282320
checked={segmentedOption.value === selected}
283321
onChange={handleChange}
284322
{...segmentedOption}

tests/index.spec.tsx

+29-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ describe('rc-segmented', () => {
163163
expect(wrapper.render()).toMatchSnapshot();
164164
expect(
165165
wrapper
166-
.find('.rc-segmented-item')
166+
.find('label.rc-segmented-item')
167167
.at(1)
168168
.hasClass('rc-segmented-item-disabled'),
169169
).toBeTruthy();
@@ -285,6 +285,34 @@ describe('rc-segmented', () => {
285285
.at(1)
286286
.simulate('change');
287287
expect(wrapper.state().value).toBe('Android');
288+
289+
// change state directly
290+
wrapper.find(Demo).setState({ value: 'Web3' });
291+
292+
// Motion end
293+
wrapper.triggerMotionEvent();
294+
act(() => {
295+
jest.runAllTimers();
296+
wrapper.update();
297+
});
298+
299+
expect(
300+
wrapper.find('.rc-segmented-item-selected').contains('Web3'),
301+
).toBeTruthy();
302+
303+
// Motion end
304+
wrapper.triggerMotionEvent();
305+
act(() => {
306+
jest.runAllTimers();
307+
wrapper.update();
308+
});
309+
310+
// change it strangely
311+
wrapper.find(Demo).setState({ value: 'Web4' });
312+
// invalid changes
313+
expect(
314+
wrapper.find('.rc-segmented-item-selected').contains('Web3'),
315+
).toBeTruthy();
288316
});
289317

290318
it('render segmented with CSSMotion', () => {

0 commit comments

Comments
 (0)