@cleartrip/ct-design-segment 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +85 -0
  2. package/dist/Segment.d.ts +3 -26
  3. package/dist/Segment.d.ts.map +1 -1
  4. package/dist/Segment.native.d.ts +5 -0
  5. package/dist/Segment.native.d.ts.map +1 -0
  6. package/dist/SegmentButton.d.ts +5 -0
  7. package/dist/SegmentButton.d.ts.map +1 -0
  8. package/dist/SegmentButton.native.d.ts +5 -0
  9. package/dist/SegmentButton.native.d.ts.map +1 -0
  10. package/dist/constants.d.ts +5 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/ct-design-segment.browser.cjs.js +1 -1
  13. package/dist/ct-design-segment.browser.cjs.js.map +1 -1
  14. package/dist/ct-design-segment.browser.esm.js +1 -1
  15. package/dist/ct-design-segment.browser.esm.js.map +1 -1
  16. package/dist/ct-design-segment.cjs.js +339 -95
  17. package/dist/ct-design-segment.cjs.js.map +1 -1
  18. package/dist/ct-design-segment.esm.js +342 -97
  19. package/dist/ct-design-segment.esm.js.map +1 -1
  20. package/dist/ct-design-segment.umd.js +1986 -128
  21. package/dist/ct-design-segment.umd.js.map +1 -1
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.native.d.ts +4 -0
  25. package/dist/index.native.d.ts.map +1 -0
  26. package/dist/style.d.ts +111 -18
  27. package/dist/style.d.ts.map +1 -1
  28. package/dist/type.d.ts +46 -0
  29. package/dist/type.d.ts.map +1 -0
  30. package/package.json +26 -10
  31. package/src/Segment.native.tsx +284 -0
  32. package/src/Segment.tsx +277 -0
  33. package/src/SegmentButton.native.tsx +110 -0
  34. package/src/SegmentButton.tsx +115 -0
  35. package/src/constants.ts +4 -0
  36. package/src/index.native.ts +3 -0
  37. package/src/index.ts +3 -0
  38. package/src/style.ts +132 -0
  39. package/src/type.ts +85 -0
@@ -0,0 +1,284 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import Animated, {
3
+ Extrapolation,
4
+ interpolate,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withTiming,
8
+ } from 'react-native-reanimated';
9
+ import { useTheme } from '@cleartrip/ct-design-theme';
10
+ import { useStyles } from '@cleartrip/ct-design-style-manager';
11
+ import { Container } from '@cleartrip/ct-design-container';
12
+
13
+ import { getSegmentVariantStyles, segmentStaticStyles, segmentThumbShadowNative } from './style';
14
+ import { ISegmentProps, type SegmentStyleConfigProps } from './type';
15
+ import { SegmentVariant } from './constants';
16
+ import { SegmentButton } from './SegmentButton';
17
+
18
+ function isEmptyOptions<T>(opts: T[] | undefined): boolean {
19
+ return !opts || opts.length === 0;
20
+ }
21
+
22
+ function toStyleArray<T>(value?: T | T[]): T[] {
23
+ if (value === undefined || value === null) return [];
24
+ return Array.isArray(value) ? value : [value];
25
+ }
26
+
27
+ const Segment: React.FC<ISegmentProps> = ({
28
+ block = false,
29
+ disabled = false,
30
+ enableTabClick = false,
31
+ variant = SegmentVariant.DEFAULT,
32
+ onChange,
33
+ options = [],
34
+ activeTabId,
35
+ animationDuration = 500,
36
+ styleConfig = {},
37
+ }) => {
38
+ const {
39
+ root = [],
40
+ optionButtonWrapper = [],
41
+ segmentButton: segmentButtonConfig,
42
+ animatedContainer = [],
43
+ animatedBlock = [],
44
+ optionButtonWrapperActive = [],
45
+ segmentButtonActive = [],
46
+ } = styleConfig || {};
47
+ const baseSegmentButtonConfig = useMemo(() => segmentButtonConfig?.segmentButton ?? {}, [segmentButtonConfig]);
48
+ const baseSegmentTextConfig = useMemo(() => segmentButtonConfig?.segmentText ?? {}, [segmentButtonConfig]);
49
+ const baseSegmentButtonRoot = useMemo(
50
+ () => toStyleArray(baseSegmentButtonConfig.root),
51
+ [baseSegmentButtonConfig.root],
52
+ );
53
+ const baseSegmentTextRoot = useMemo(() => toStyleArray(baseSegmentTextConfig.root), [baseSegmentTextConfig.root]);
54
+
55
+ const theme = useTheme();
56
+ const variantStyles = useMemo(() => getSegmentVariantStyles(variant, theme), [variant, theme]);
57
+
58
+ const sanitizedOptions = useMemo(
59
+ () => options.filter((option) => String(option.tabId ?? '').trim().length > 0),
60
+ [options],
61
+ );
62
+
63
+ const [activeTabEleId, setActiveTabEleId] = useState<string>(() => {
64
+ const t = typeof activeTabId === 'string' ? activeTabId.trim() : '';
65
+ return t.length > 0 ? t : '';
66
+ });
67
+ const [trackContentWidth, setTrackContentWidth] = useState<number>(0);
68
+ const [tabLayouts, setTabLayouts] = useState<Array<{ x: number; width: number }>>([]);
69
+
70
+ const findActiveIndex = useCallback(
71
+ (id: string) => {
72
+ const idx = sanitizedOptions.findIndex((item) => item.tabId === id.trim());
73
+ return idx === -1 ? 0 : idx;
74
+ },
75
+ [sanitizedOptions],
76
+ );
77
+
78
+ const animation = useSharedValue(0);
79
+
80
+ const handleOptionClick = useCallback(
81
+ (tabId: string, index: number) => {
82
+ const clickedOption = sanitizedOptions[index];
83
+ const isDisabled = clickedOption?.disabled ?? false;
84
+
85
+ if (isDisabled && !enableTabClick) return;
86
+ onChange(tabId);
87
+ if (!isDisabled) {
88
+ setActiveTabEleId(tabId || '');
89
+ animation.value = withTiming(index, {
90
+ duration: animationDuration,
91
+ });
92
+ }
93
+ },
94
+ [animation, animationDuration, enableTabClick, onChange, sanitizedOptions],
95
+ );
96
+
97
+ const handleTrackContentLayout = useCallback(
98
+ (event: { nativeEvent?: { layout: { width: number } }; layout?: { width: number } }) => {
99
+ const width = event.nativeEvent?.layout?.width ?? event.layout?.width ?? 0;
100
+ setTrackContentWidth(width);
101
+ },
102
+ [],
103
+ );
104
+ const handleTabLayout = useCallback(
105
+ (
106
+ index: number,
107
+ event: { nativeEvent?: { layout: { x: number; width: number } }; layout?: { x: number; width: number } },
108
+ ) => {
109
+ const x = event.nativeEvent?.layout?.x ?? event.layout?.x ?? 0;
110
+ const width = event.nativeEvent?.layout?.width ?? event.layout?.width ?? 0;
111
+ setTabLayouts((previousLayouts) => {
112
+ const existingLayout = previousLayouts[index];
113
+ if (existingLayout && existingLayout.x === x && existingLayout.width === width) {
114
+ return previousLayouts;
115
+ }
116
+ const nextLayouts = [...previousLayouts];
117
+ nextLayouts[index] = { x, width };
118
+ return nextLayouts;
119
+ });
120
+ },
121
+ [],
122
+ );
123
+
124
+ const optionLength = sanitizedOptions.length;
125
+ const activeIndex = findActiveIndex(activeTabEleId);
126
+
127
+ const dynamicStyles = useStyles(
128
+ (themeArg) => ({
129
+ root: {
130
+ width: block ? themeArg?.size['100P'] : 'auto',
131
+ },
132
+ animatedBlock: {
133
+ ...(variantStyles.animatedBlock?.style ?? {}),
134
+ },
135
+ }),
136
+ [block, variantStyles, optionLength],
137
+ );
138
+ const tabCountForAnim = sanitizedOptions.length;
139
+
140
+ const animatedStyle = useAnimatedStyle(() => {
141
+ const optionsCount = Math.max(tabCountForAnim, 1);
142
+ const indices = Array.from({ length: optionsCount }, (_, index) => index);
143
+ const fallbackSegmentWidth = trackContentWidth / optionsCount;
144
+ const hasStableMeasurements =
145
+ tabLayouts.length >= optionsCount &&
146
+ tabLayouts.slice(0, optionsCount).every((layout) => !!layout && layout.width > 0);
147
+ const positions = hasStableMeasurements
148
+ ? indices.map((index) => tabLayouts[index]?.x ?? index * fallbackSegmentWidth)
149
+ : indices.map((index) => index * fallbackSegmentWidth);
150
+ const widths = hasStableMeasurements
151
+ ? indices.map((index) => tabLayouts[index]?.width ?? fallbackSegmentWidth)
152
+ : indices.map(() => fallbackSegmentWidth);
153
+ const interpolatedLeft = interpolate(animation.value, indices, positions, Extrapolation.CLAMP);
154
+ const interpolatedWidth = Math.max(interpolate(animation.value, indices, widths, Extrapolation.CLAMP), 0);
155
+ const maxLeft = Math.max(trackContentWidth - interpolatedWidth, 0);
156
+ const boundedLeft = Math.min(Math.max(interpolatedLeft, 0), maxLeft);
157
+
158
+ return {
159
+ left: boundedLeft,
160
+ width: interpolatedWidth,
161
+ };
162
+ }, [trackContentWidth, tabCountForAnim, tabLayouts]);
163
+
164
+ const getSegmentButtonStyleConfig = useCallback(
165
+ (index: number): SegmentStyleConfigProps['segmentButton'] => {
166
+ const isActive = index === activeIndex;
167
+ return {
168
+ segmentButton: {
169
+ ...baseSegmentButtonConfig,
170
+ root: [...optionButtonWrapper, ...(isActive ? optionButtonWrapperActive : []), ...baseSegmentButtonRoot],
171
+ },
172
+ segmentText: {
173
+ ...baseSegmentTextConfig,
174
+ root: [
175
+ ...(variantStyles.segmentButton?.style ? [variantStyles.segmentButton.style] : []),
176
+ ...(isActive && variantStyles.segmentButtonActive?.style ? [variantStyles.segmentButtonActive.style] : []),
177
+ ...(isActive ? segmentButtonActive : []),
178
+ ...baseSegmentTextRoot,
179
+ ],
180
+ },
181
+ };
182
+ },
183
+ [
184
+ activeIndex,
185
+ baseSegmentButtonConfig,
186
+ baseSegmentButtonRoot,
187
+ baseSegmentTextConfig,
188
+ baseSegmentTextRoot,
189
+ optionButtonWrapper,
190
+ optionButtonWrapperActive,
191
+ segmentButtonActive,
192
+ variantStyles.segmentButton?.style,
193
+ variantStyles.segmentButtonActive?.style,
194
+ ],
195
+ );
196
+
197
+ useEffect(() => {
198
+ if (activeTabId === undefined || activeTabId === null) return;
199
+ const t = `${activeTabId}`.trim();
200
+ if (!t.length) return;
201
+ if (sanitizedOptions.some((o) => o.tabId === t)) {
202
+ setActiveTabEleId(t);
203
+ }
204
+ }, [activeTabId, sanitizedOptions]);
205
+
206
+ useEffect(() => {
207
+ setActiveTabEleId((prev) => {
208
+ const trimmed = prev.trim();
209
+ if (trimmed.length > 0 && sanitizedOptions.some((option) => option.tabId === trimmed)) {
210
+ return prev;
211
+ }
212
+ return sanitizedOptions[0]?.tabId ?? '';
213
+ });
214
+ }, [sanitizedOptions]);
215
+
216
+ useEffect(() => {
217
+ setTabLayouts([]);
218
+ }, [sanitizedOptions]);
219
+
220
+ useEffect(() => {
221
+ if (!sanitizedOptions.length) return;
222
+ const idx = Math.min(Math.max(findActiveIndex(activeTabEleId), 0), sanitizedOptions.length - 1);
223
+ animation.value = withTiming(idx, { duration: animationDuration });
224
+ }, [activeTabEleId, animation, animationDuration, findActiveIndex, sanitizedOptions]);
225
+
226
+ if (!sanitizedOptions?.length) return null;
227
+
228
+ return (
229
+ <Container
230
+ styleConfig={{
231
+ root: [
232
+ segmentStaticStyles.root,
233
+ dynamicStyles.root,
234
+ ...(variantStyles.root?.style ? [variantStyles.root.style] : []),
235
+ ...root,
236
+ ],
237
+ }}
238
+ >
239
+ <Container
240
+ onLayout={handleTrackContentLayout}
241
+ styleConfig={{
242
+ root: [segmentStaticStyles.animatedContainer, ...animatedContainer],
243
+ }}
244
+ >
245
+ <Animated.View
246
+ pointerEvents='none'
247
+ style={[
248
+ segmentStaticStyles.animatedBlock,
249
+ animatedStyle,
250
+ dynamicStyles.animatedBlock,
251
+ ...(variantStyles.animatedBlock?.style ? [variantStyles.animatedBlock.style] : []),
252
+ segmentThumbShadowNative,
253
+ ...animatedBlock,
254
+ ]}
255
+ />
256
+ {!isEmptyOptions(sanitizedOptions) &&
257
+ sanitizedOptions.map((option, index) => (
258
+ <Container
259
+ key={option.tabId}
260
+ onLayout={(event) => {
261
+ handleTabLayout(index, event);
262
+ }}
263
+ styleConfig={{
264
+ root: [segmentStaticStyles.tabMeasureCell],
265
+ }}
266
+ >
267
+ <SegmentButton
268
+ option={option}
269
+ index={index}
270
+ activeIndex={activeIndex}
271
+ disabled={disabled}
272
+ onOptionClick={handleOptionClick}
273
+ optionLength={optionLength}
274
+ variant={variant}
275
+ styleConfig={getSegmentButtonStyleConfig(index)}
276
+ />
277
+ </Container>
278
+ ))}
279
+ </Container>
280
+ </Container>
281
+ );
282
+ };
283
+
284
+ export default Segment;
@@ -0,0 +1,277 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+
3
+ import { useTheme } from '@cleartrip/ct-design-theme';
4
+ import { useStyles, useWebMergeStyles } from '@cleartrip/ct-design-style-manager';
5
+ import { Container } from '@cleartrip/ct-design-container';
6
+
7
+ import { getSegmentVariantStyles, segmentStaticStyles, segmentWebThumbStyles } from './style';
8
+ import { ISegmentProps } from './type';
9
+ import { SegmentVariant } from './constants';
10
+ import { SegmentButton } from './SegmentButton';
11
+ import { css } from '@emotion/css';
12
+ import useIsomorphicEffect from '@cleartrip/ct-design-use-isomorphic-effect';
13
+
14
+ function isEmptyOptions<T>(opts: T[] | undefined): boolean {
15
+ return !opts || opts.length === 0;
16
+ }
17
+
18
+ function toStyleArray<T>(value?: T | T[]): T[] {
19
+ if (value === undefined || value === null) return [];
20
+ return Array.isArray(value) ? value : [value];
21
+ }
22
+
23
+ const Segment: React.FC<ISegmentProps> = ({
24
+ block = false,
25
+ disabled = false,
26
+ enableTabClick = false,
27
+ variant = SegmentVariant.DEFAULT,
28
+ onChange,
29
+ options = [],
30
+ activeTabId,
31
+ animationDuration = 500,
32
+ styleConfig = {},
33
+ }) => {
34
+ const {
35
+ root = [],
36
+ optionButtonWrapper = [],
37
+ segmentButton: segmentButtonConfig,
38
+ animatedContainer = [],
39
+ animatedBlock = [],
40
+ optionButtonWrapperActive = [],
41
+ segmentButtonActive = [],
42
+ } = styleConfig || {};
43
+ const theme = useTheme();
44
+ const variantStyles = useMemo(() => getSegmentVariantStyles(variant, theme), [variant, theme]);
45
+
46
+ const rootRef = useRef<HTMLDivElement | null>(null);
47
+ const tabRefs = useRef<Array<HTMLDivElement | null>>([]);
48
+
49
+ const sanitizedOptions = useMemo(
50
+ () => options.filter((option) => String(option.tabId ?? '').trim().length > 0),
51
+ [options],
52
+ );
53
+
54
+ const [activeTabEleId, setActiveTabEleId] = useState<string>(() => {
55
+ const t = typeof activeTabId === 'string' ? activeTabId.trim() : '';
56
+ return t.length > 0 ? t : '';
57
+ });
58
+ const [trackWidth, setTrackWidth] = useState<number>(0);
59
+ const [tabLayouts, setTabLayouts] = useState<Array<{ x: number; width: number }>>([]);
60
+
61
+ const baseSegmentButtonConfig = useMemo(() => segmentButtonConfig?.segmentButton ?? {}, [segmentButtonConfig]);
62
+ const baseSegmentTextConfig = useMemo(() => segmentButtonConfig?.segmentText ?? {}, [segmentButtonConfig]);
63
+ const baseSegmentButtonRoot = useMemo(
64
+ () => toStyleArray(baseSegmentButtonConfig.root),
65
+ [baseSegmentButtonConfig.root],
66
+ );
67
+ const baseSegmentTextRoot = useMemo(() => toStyleArray(baseSegmentTextConfig.root), [baseSegmentTextConfig.root]);
68
+
69
+ const measureTabs = useCallback(() => {
70
+ const rootNode = rootRef.current;
71
+ if (!rootNode) return;
72
+ const rootRect = rootNode.getBoundingClientRect();
73
+ setTrackWidth(rootRect.width);
74
+ const layouts = sanitizedOptions.map((_, index) => {
75
+ const tabNode = tabRefs.current[index];
76
+ if (!tabNode) return { x: 0, width: 0 };
77
+ const rect = tabNode.getBoundingClientRect();
78
+ return { x: rect.left - rootRect.left, width: rect.width };
79
+ });
80
+ setTabLayouts(layouts);
81
+ }, [sanitizedOptions]);
82
+
83
+ useEffect(() => {
84
+ tabRefs.current = tabRefs.current.slice(0, sanitizedOptions.length);
85
+ }, [sanitizedOptions.length]);
86
+
87
+ useEffect(() => {
88
+ measureTabs();
89
+ }, [measureTabs]);
90
+
91
+ useEffect(() => {
92
+ if (typeof ResizeObserver === 'undefined' || !rootRef.current) return;
93
+ const observer = new ResizeObserver(() => {
94
+ measureTabs();
95
+ });
96
+ observer.observe(rootRef.current);
97
+ tabRefs.current.forEach((tabNode) => {
98
+ if (tabNode) observer.observe(tabNode);
99
+ });
100
+ return () => observer.disconnect();
101
+ }, [measureTabs, sanitizedOptions.length]);
102
+
103
+ const findActiveIndex = useCallback(
104
+ (id: string) => {
105
+ const idx = sanitizedOptions.findIndex((item) => item.tabId === id.trim());
106
+ return idx === -1 ? 0 : idx;
107
+ },
108
+ [sanitizedOptions],
109
+ );
110
+
111
+ const handleOptionClick = useCallback(
112
+ (tabId: string, index: number) => {
113
+ const clickedOption = sanitizedOptions[index];
114
+ const isDisabled = clickedOption?.disabled ?? false;
115
+
116
+ if (isDisabled && !enableTabClick) return;
117
+ onChange(tabId);
118
+ if (!isDisabled) {
119
+ setActiveTabEleId(tabId || '');
120
+ }
121
+ },
122
+ [enableTabClick, onChange, sanitizedOptions],
123
+ );
124
+
125
+ const optionLength = sanitizedOptions.length;
126
+ const activeIndex = findActiveIndex(activeTabEleId);
127
+
128
+ const dynamicStyles = useStyles(
129
+ () => ({
130
+ root: {
131
+ width: block ? theme?.size['100P'] : 'auto',
132
+ },
133
+ }),
134
+ [block, theme],
135
+ );
136
+
137
+ const getSegmentButtonStyleConfig = useCallback(
138
+ (index: number) => {
139
+ const isActive = index === activeIndex;
140
+ return {
141
+ segmentButton: {
142
+ ...baseSegmentButtonConfig,
143
+ root: [...optionButtonWrapper, ...(isActive ? optionButtonWrapperActive : []), ...baseSegmentButtonRoot],
144
+ },
145
+ segmentText: {
146
+ ...baseSegmentTextConfig,
147
+ root: [
148
+ ...(variantStyles.segmentButton?.style ? [variantStyles.segmentButton.style] : []),
149
+ ...(isActive && variantStyles.segmentButtonActive?.style ? [variantStyles.segmentButtonActive.style] : []),
150
+ ...(isActive ? segmentButtonActive : []),
151
+ ...baseSegmentTextRoot,
152
+ ],
153
+ },
154
+ };
155
+ },
156
+ [
157
+ activeIndex,
158
+ baseSegmentButtonConfig,
159
+ baseSegmentButtonRoot,
160
+ baseSegmentTextConfig,
161
+ baseSegmentTextRoot,
162
+ optionButtonWrapper,
163
+ optionButtonWrapperActive,
164
+ segmentButtonActive,
165
+ variantStyles.segmentButton?.style,
166
+ variantStyles.segmentButtonActive?.style,
167
+ ],
168
+ );
169
+
170
+ useIsomorphicEffect(() => {
171
+ if (activeTabId === undefined || activeTabId === null) return;
172
+ const t = `${activeTabId}`.trim();
173
+ if (!t.length) return;
174
+ if (sanitizedOptions.some((option) => option.tabId === t)) {
175
+ setActiveTabEleId(t);
176
+ return;
177
+ }
178
+ }, [activeTabId, sanitizedOptions]);
179
+
180
+ useIsomorphicEffect(() => {
181
+ setActiveTabEleId((prev) => {
182
+ const trimmed = prev.trim();
183
+ if (trimmed.length > 0 && sanitizedOptions.some((option) => option.tabId === trimmed)) {
184
+ return prev;
185
+ }
186
+ return sanitizedOptions[0]?.tabId ?? '';
187
+ });
188
+ }, [sanitizedOptions]);
189
+
190
+ const rootClassName = useWebMergeStyles(
191
+ [
192
+ segmentStaticStyles.root,
193
+ dynamicStyles.root,
194
+ ...(variantStyles.root?.style ? [variantStyles.root.style] : []),
195
+ ...root,
196
+ ],
197
+ [segmentStaticStyles.root, dynamicStyles.root, variantStyles.root?.style, root],
198
+ );
199
+ const tabMeasureCellClassName = useWebMergeStyles(
200
+ [segmentStaticStyles.tabMeasureCell],
201
+ [segmentStaticStyles.tabMeasureCell],
202
+ );
203
+ const webThumbBaseClassName = useWebMergeStyles(
204
+ [
205
+ segmentWebThumbStyles.root,
206
+ ...(variantStyles.animatedBlock?.style ? [variantStyles.animatedBlock.style] : []),
207
+ ...animatedBlock,
208
+ ],
209
+ [segmentWebThumbStyles.root, variantStyles.animatedBlock?.style, animatedBlock],
210
+ );
211
+
212
+ const hasStableMeasurements =
213
+ tabLayouts.length >= optionLength &&
214
+ tabLayouts.slice(0, optionLength).every((layout) => !!layout && layout.width > 0);
215
+ const segmentPercent = optionLength > 0 ? 100 / optionLength : 100;
216
+
217
+ const dynamicWebStyles = useMemo(() => {
218
+ const transition = `left ${animationDuration}ms ease, width ${animationDuration}ms ease`;
219
+ if (hasStableMeasurements && optionLength > 0) {
220
+ const safeIndex = Math.max(0, Math.min(activeIndex, optionLength - 1));
221
+ const activeLayout = tabLayouts[safeIndex];
222
+ const width = Math.max(activeLayout?.width ?? trackWidth / Math.max(optionLength, 1), 0);
223
+ const maxLeft = Math.max(trackWidth - width, 0);
224
+ const left = Math.min(Math.max(activeLayout?.x ?? 0, 0), maxLeft);
225
+ return css({
226
+ left: `${left}px`,
227
+ width: `${width}px`,
228
+ transition,
229
+ });
230
+ }
231
+
232
+ return css({
233
+ left: `calc(${activeIndex * segmentPercent}%)`,
234
+ width: `${segmentPercent}%`,
235
+ transition,
236
+ animationDuration: `${animationDuration}ms`,
237
+ animationFillMode: 'forwards',
238
+ });
239
+ }, [activeIndex, animationDuration, hasStableMeasurements, optionLength, segmentPercent, tabLayouts, trackWidth]);
240
+
241
+ if (!sanitizedOptions?.length) return null;
242
+
243
+ return (
244
+ <div className={rootClassName} ref={rootRef}>
245
+ <Container
246
+ styleConfig={{
247
+ root: [segmentStaticStyles.animatedContainer, ...animatedContainer],
248
+ }}
249
+ >
250
+ {!isEmptyOptions(sanitizedOptions) &&
251
+ sanitizedOptions.map((option, index) => (
252
+ <div
253
+ key={option.tabId}
254
+ ref={(element) => {
255
+ tabRefs.current[index] = element;
256
+ }}
257
+ className={tabMeasureCellClassName}
258
+ >
259
+ <SegmentButton
260
+ option={option}
261
+ index={index}
262
+ activeIndex={activeIndex}
263
+ disabled={disabled}
264
+ onOptionClick={handleOptionClick}
265
+ optionLength={optionLength}
266
+ variant={variant}
267
+ styleConfig={getSegmentButtonStyleConfig(index)}
268
+ />
269
+ </div>
270
+ ))}
271
+ </Container>
272
+ <div className={`${webThumbBaseClassName} ${dynamicWebStyles}`} />
273
+ </div>
274
+ );
275
+ };
276
+
277
+ export default Segment;
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+
3
+ import { Button, ButtonColor, ButtonSize, ButtonVariant } from '@cleartrip/ct-design-button';
4
+ import { useStyles } from '@cleartrip/ct-design-style-manager';
5
+ import { useTheme } from '@cleartrip/ct-design-theme';
6
+
7
+ import { ISegmentButtonProps } from './type';
8
+ import { SegmentVariant } from './constants';
9
+ import { getStyledSegmentButtonStyles, segmentStaticStyles } from './style';
10
+
11
+ export const SegmentButton: React.FC<ISegmentButtonProps> = ({
12
+ option,
13
+ index,
14
+ activeIndex,
15
+ disabled,
16
+ onOptionClick,
17
+ styleConfig,
18
+ variant,
19
+ optionLength: _optionLength,
20
+ }) => {
21
+ const theme = useTheme();
22
+ const isActive = index === activeIndex;
23
+
24
+ const { segmentButton = {}, segmentText = {} } = styleConfig || {};
25
+
26
+ const shell = useStyles(
27
+ (t) => ({
28
+ rowCell: {
29
+ flexGrow: 1,
30
+ flexBasis: 0,
31
+ flexShrink: 1,
32
+ minWidth: 0,
33
+ minHeight: 0,
34
+ width: undefined,
35
+ alignSelf: 'stretch',
36
+ height: t.size[10],
37
+ maxHeight: t.size[10],
38
+ paddingHorizontal: 0,
39
+ paddingTop: 0,
40
+ paddingBottom: 0,
41
+ paddingVertical: 0,
42
+ marginHorizontal: 0,
43
+ justifyContent: 'center',
44
+ alignItems: 'center',
45
+ },
46
+ }),
47
+ [],
48
+ );
49
+
50
+ const dynamicStyles = useStyles(
51
+ (innerTheme) => {
52
+ if (variant === SegmentVariant.DARK) {
53
+ return {
54
+ typography: {
55
+ fontSize: innerTheme.typography.size[14],
56
+ fontWeight: innerTheme.typography.weight.medium,
57
+ lineHeight: innerTheme.size[5],
58
+ textAlign: 'center' as const,
59
+ textAlignVertical: 'center' as const,
60
+ includeFontPadding: false,
61
+ paddingVertical: 0,
62
+ marginVertical: 0,
63
+ color: isActive ? theme.color.text.neutral : innerTheme.color.text.primary,
64
+ },
65
+ };
66
+ }
67
+ return {
68
+ typography: {
69
+ ...getStyledSegmentButtonStyles({ theme: innerTheme, isActive }),
70
+ textAlign: 'center' as const,
71
+ textAlignVertical: 'center' as const,
72
+ includeFontPadding: false,
73
+ paddingVertical: 0,
74
+ marginVertical: 0,
75
+ },
76
+ };
77
+ },
78
+ [isActive, variant, theme],
79
+ );
80
+
81
+ const segmentTextRoot =
82
+ segmentText.root === undefined ? [] : Array.isArray(segmentText.root) ? segmentText.root : [segmentText.root];
83
+
84
+ return (
85
+ <Button
86
+ variant={ButtonVariant.BARE}
87
+ color={ButtonColor.TERTIARY}
88
+ size={ButtonSize.SMALL}
89
+ isFullWidth={false}
90
+ prefixIcon={option?.icon}
91
+ onClick={() => {
92
+ if (disabled) return;
93
+ onOptionClick(option.tabId, index);
94
+ }}
95
+ disabled={disabled}
96
+ styleConfig={{
97
+ ...segmentButton,
98
+ root: [segmentStaticStyles.button, ...(segmentButton.root ?? []), shell.rowCell],
99
+ typography: {
100
+ ...segmentText,
101
+ root: [dynamicStyles.typography, ...segmentTextRoot],
102
+ },
103
+ }}
104
+ >
105
+ {option?.text ?? ''}
106
+ </Button>
107
+ );
108
+ };
109
+
110
+ export default SegmentButton;