-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathindex.tsx
More file actions
105 lines (92 loc) · 2.69 KB
/
index.tsx
File metadata and controls
105 lines (92 loc) · 2.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { type ReactNode } from 'react'
import styled, { css } from 'styled-components'
export type SegmentedControlOption<T extends string = string> = {
label: ReactNode
value: T
disabled?: boolean
}
type SegmentedControlProps<T extends string> = {
onChange?: (value: T) => void
options?: readonly SegmentedControlOption<T>[]
size?: 'sm' | 'md'
value?: T
}
const sizeStyles = {
sm: css`
padding: 8px;
`,
md: css`
padding: 8px 12px;
`,
}
const Container = styled.div<{ $optionCount: number }>`
display: grid;
position: relative;
grid-template-columns: repeat(${({ $optionCount }) => $optionCount}, minmax(0, 1fr));
align-items: center;
border: 1px solid ${({ theme }) => theme.border};
border-radius: 999px;
background: ${({ theme }) => theme.background};
`
const ActivePill = styled.div<{ $activeIndex: number; $optionCount: number }>`
position: absolute;
top: 1px;
bottom: 1px;
left: 1px;
width: calc((100% - 2px) / ${({ $optionCount }) => $optionCount});
border-radius: 999px;
background: ${({ theme }) => theme.tabActive};
transform: translateX(calc(100% * ${({ $activeIndex }) => $activeIndex}));
transition: transform 200ms ease, background 200ms ease;
pointer-events: none;
`
const OptionButton = styled.button<{ $active: boolean; $size: 'sm' | 'md' }>`
position: relative;
z-index: 1;
min-width: 48px;
border: 0;
border-radius: 999px;
background: transparent;
color: ${({ theme, $active }) => ($active ? theme.text : theme.subText)};
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: color 200ms ease, background 200ms ease;
${({ $size }) => sizeStyles[$size]}
:hover:not(:disabled) {
background: ${({ theme, $active }) => ($active ? 'transparent' : theme.buttonGray)};
}
:disabled {
cursor: not-allowed;
opacity: 0.5;
}
`
const SegmentedControl = <T extends string>({
onChange,
options = [],
size = 'sm',
value,
}: SegmentedControlProps<T>) => {
if (!options.length) return null
const activeIndex = options.findIndex(option => option.value === value)
return (
<Container $optionCount={options.length} role="tablist">
<ActivePill $activeIndex={Math.max(activeIndex, 0)} $optionCount={options.length} />
{options.map(option => (
<OptionButton
$active={option.value === value}
$size={size}
aria-selected={option.value === value}
disabled={option.disabled || !onChange}
key={option.value}
onClick={() => !option.disabled && onChange?.(option.value)}
role="tab"
type="button"
>
{option.label}
</OptionButton>
))}
</Container>
)
}
export default SegmentedControl