Skip to content

Commit 22a6c20

Browse files
committed
feat: add highlight active segment with transition
1 parent 13d29a6 commit 22a6c20

File tree

2 files changed

+107
-55
lines changed

2 files changed

+107
-55
lines changed

src/Common/SegmentedControl/NSegmentedControl.component.tsx

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactElement, useState } from 'react'
1+
import { ReactElement, useEffect, useRef, useState } from 'react'
22
import { Icon, IconsProps, SelectPickerOptionType } from '@Shared/Components'
33
import { ComponentSizeType } from '@Shared/constants'
44
import './segmentedControl.scss'
@@ -36,6 +36,7 @@ export interface NSegmentedControlProps {
3636
onChange?: (selectedTab: SegmentType) => void
3737
name: string
3838
size?: Extract<ComponentSizeType, ComponentSizeType.xs | ComponentSizeType.small | ComponentSizeType.medium>
39+
fullWidth?: boolean
3940
}
4041

4142
export const COMPONENT_SIZE_TO_SEGMENT_CLASS_MAP: Record<NSegmentedControlProps['size'], string> = {
@@ -62,70 +63,88 @@ const NSegmentedControl = ({
6263
name,
6364
size = ComponentSizeType.medium,
6465
value: controlledValue,
66+
fullWidth = false,
6567
}: NSegmentedControlProps) => {
68+
const segmentedControlRefContainer = useRef<HTMLDivElement>(null)
69+
const selectedSegmentRef = useRef<HTMLDivElement>(null)
6670
const [selectedSegmentValue, setSelectedSegmentValue] = useState<SegmentType['value'] | null>(segments[0].value)
6771
const segmentValue = controlledValue === undefined ? selectedSegmentValue : controlledValue
6872

73+
useEffect(() => {
74+
if (segmentValue) {
75+
const { offsetWidth, offsetLeft } = selectedSegmentRef.current
76+
const { style } = segmentedControlRefContainer.current
77+
78+
style.setProperty('--segmented-control-highlight-width', `${offsetWidth}px`)
79+
style.setProperty('--segmented-control-highlight-x-position', `${offsetLeft}px`)
80+
}
81+
}, [segmentValue, size, fullWidth])
82+
6983
const handleSegmentChange = (updatedSegment: SegmentType) => {
7084
setSelectedSegmentValue(updatedSegment.value)
7185
onChange?.(updatedSegment)
7286
}
7387

7488
return (
7589
<div
76-
className={`segmented-control dc__inline-flex dc__content-center dc__align-items-center dc__gap-2 br-6 ${size === ComponentSizeType.xs ? 'p-1' : 'p-2'}`}
90+
className={`segmented-control ${!fullWidth ? 'dc__inline-flex' : ''} br-6 ${size === ComponentSizeType.xs ? 'p-1' : 'p-2'}`}
7791
>
78-
{segments.map((segment) => {
79-
const { value, icon, isError, label, tooltipProps, ariaLabel } = segment
80-
const isSelected = value === segmentValue
92+
<div
93+
className="segmented-control__container flex left dc__position-rel dc__align-items-center dc__gap-2"
94+
ref={segmentedControlRefContainer}
95+
>
96+
{segments.map((segment) => {
97+
const { value, icon, isError, label, tooltipProps, ariaLabel } = segment
98+
const isSelected = value === segmentValue
8199

82-
return (
83-
<ConditionalWrap
84-
key={value}
85-
condition={!!tooltipProps?.content}
86-
wrap={wrapWithTooltip(tooltipProps)}
87-
>
88-
<div
89-
// ref={item.ref}
90-
className="dc__position-rel dc__text-center"
100+
return (
101+
<ConditionalWrap
102+
key={value}
103+
condition={!!tooltipProps?.content}
104+
wrap={wrapWithTooltip(tooltipProps)}
91105
>
92-
<input
93-
type="radio"
94-
value={value}
95-
id={`${name}-${value}`}
96-
name={name}
97-
onChange={() => handleSegmentChange(segment)}
98-
checked={isSelected}
99-
className="dc__opacity-0 m-0-imp dc__top-0 dc__left-0 dc__position-abs dc__bottom-0 dc__right-0 w-100 pointer h-100 dc__visibility-hidden"
100-
/>
101-
102-
<label
103-
htmlFor={`${name}-${value}`}
104-
className={`pointer m-0 flex left dc__gap-4 br-4 segmented-control__segment segmented-control__segment--${size} ${isSelected ? 'bg__primary fw-6 segmented-control__segment--selected' : 'fw-4'} ${segment.isError ? 'cr-5' : 'cn-9'} ${COMPONENT_SIZE_TO_SEGMENT_CLASS_MAP[size]}`}
105-
aria-label={ariaLabel}
106+
<div
107+
className={`dc__position-rel dc__text-center ${fullWidth ? 'flex-grow-1' : ''}`}
108+
ref={isSelected ? selectedSegmentRef : undefined}
106109
>
107-
{(isError || icon) && (
108-
<span className={`flex ${COMPONENT_SIZE_TO_ICON_CLASS_MAP[size]}`}>
109-
<Icon
110-
{...(isError
111-
? {
112-
name: 'ic-error',
113-
color: null,
114-
}
115-
: {
116-
name: icon,
117-
color: isSelected ? 'N900' : 'N700',
118-
})}
119-
size={size === ComponentSizeType.xs ? 14 : 16}
120-
/>
121-
</span>
122-
)}
123-
{label && <span>{label}</span>}
124-
</label>
125-
</div>
126-
</ConditionalWrap>
127-
)
128-
})}
110+
<input
111+
type="radio"
112+
value={value}
113+
id={`${name}-${value}`}
114+
name={name}
115+
onChange={() => handleSegmentChange(segment)}
116+
checked={isSelected}
117+
className="dc__opacity-0 m-0-imp dc__top-0 dc__left-0 dc__position-abs dc__bottom-0 dc__right-0 w-100 pointer h-100 dc__visibility-hidden"
118+
/>
119+
120+
<label
121+
htmlFor={`${name}-${value}`}
122+
className={`pointer m-0 flex ${!fullWidth ? 'left' : ''} dc__gap-4 br-4 segmented-control__segment segmented-control__segment--${size} ${isSelected ? 'fw-6 segmented-control__segment--selected' : 'fw-4'} ${segment.isError ? 'cr-5' : 'cn-9'} ${COMPONENT_SIZE_TO_SEGMENT_CLASS_MAP[size]}`}
123+
aria-label={ariaLabel}
124+
>
125+
{(isError || icon) && (
126+
<span className={`flex ${COMPONENT_SIZE_TO_ICON_CLASS_MAP[size]}`}>
127+
<Icon
128+
{...(isError
129+
? {
130+
name: 'ic-error',
131+
color: null,
132+
}
133+
: {
134+
name: icon,
135+
color: isSelected ? 'N900' : 'N700',
136+
})}
137+
size={size === ComponentSizeType.xs ? 14 : 16}
138+
/>
139+
</span>
140+
)}
141+
{label && <span>{label}</span>}
142+
</label>
143+
</div>
144+
</ConditionalWrap>
145+
)
146+
})}
147+
</div>
129148
</div>
130149
)
131150
}

src/Common/SegmentedControl/segmentedControl.scss

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,52 @@
11
.segmented-control {
2+
$segmented-control-selector: &;
3+
24
background: var(--bg-segmented-control);
35

6+
&__container {
7+
--segmented-control-highlight-width: auto;
8+
--segmented-control-highlight-x-position: 0;
9+
10+
&::before {
11+
content: '';
12+
background: var(--bg-primary);
13+
border-radius: 4px;
14+
width: var(--segmented-control-highlight-width);
15+
transform: translateX(var(--segmented-control-highlight-x-position));
16+
position: absolute;
17+
left: 0;
18+
z-index: 0;
19+
border: 0.5px solid var(--border-secondary);
20+
box-shadow: 0px 1px 2px 0px var(--black-20);
21+
height: 100%;
22+
transition: transform 0.3s ease, width 0.3s ease;
23+
24+
&:has(#{$segmented-control-selector}__segment--xs) {
25+
top: 1px;
26+
bottom: 1px;
27+
}
28+
29+
&:has(#{$segmented-control-selector}__segment--small),
30+
&:has(#{$segmented-control-selector}__segment--medium) {
31+
top: 2px;
32+
bottom: 2px;
33+
}
34+
}
35+
}
36+
437
&__segment {
538
$parent-selector: &;
39+
transition: color 0.3s ease;
640

7-
&:hover {
41+
&:hover:not(#{$parent-selector}--selected) {
842
background-color: var(--bg-secondary);
943
}
1044

1145
&--selected {
12-
border: 0.5px solid var(--border-secondary);
13-
box-shadow: 0px 1px 2px 0px var(--black-20);
14-
1546
&#{$parent-selector} {
16-
&--xs, &--small {
47+
48+
&--xs,
49+
&--small {
1750
padding-block: 1.5px;
1851
padding-inline: 5.5px;
1952
}

0 commit comments

Comments
 (0)