1
- import { ChangeEvent , SessionDataTestId , SyntheticEvent } from 'react' ;
1
+ import { ChangeEvent , SessionDataTestId , type MouseEventHandler } from 'react' ;
2
2
3
3
import styled , { CSSProperties } from 'styled-components' ;
4
4
import { Flex } from './Flex' ;
5
5
6
- const StyledButton = styled . button < { disabled : boolean } > `
6
+ const StyledContainer = styled . div < { disabled : boolean } > `
7
7
cursor: ${ props => ( props . disabled ? 'not-allowed' : 'pointer' ) } ;
8
8
min-height: 30px;
9
9
background-color: var(--transparent-color);
10
10
` ;
11
11
12
- const StyledInput = styled . input < {
13
- filledSize : number ;
14
- outlineOffset : number ;
15
- selectedColor ?: string ;
12
+ const StyledRadioOuter = styled . div < { $disabled : boolean ; $diameterRadioBorder : number } > `
13
+ width: ${ props => props . $diameterRadioBorder } px;
14
+ height: ${ props => props . $diameterRadioBorder } px;
15
+ border: 1px solid var(--text-primary-color);
16
+ border-radius: 50%;
17
+ background-color: transparent;
18
+ cursor: ${ props => ( props . $disabled ? 'not-allowed' : 'pointer' ) } ;
19
+ position: relative;
20
+ ` ;
21
+
22
+ const StyledSelectedInner = styled . div < {
23
+ $disabled : boolean ;
24
+ $selected : boolean ;
25
+ $diameterRadioBg : number ;
16
26
} > `
17
- opacity: 0;
27
+ width: ${ props => props . $diameterRadioBg } px;
28
+ height: ${ props => props . $diameterRadioBg } px;
29
+ border-radius: 50%;
30
+ opacity: ${ props => ( props . $selected ? 1 : 0 ) } ;
31
+ background: ${ props => ( props . $disabled ? 'var(--disabled-color)' : 'var(--primary-color)' ) } ;
32
+ pointer-events: none;
33
+ transition-duration: var(--default-duration);
18
34
position: absolute;
19
- width: ${ props => props . filledSize + props . outlineOffset } px;
20
- height: ${ props => props . filledSize + props . outlineOffset } px;
21
-
22
- &:checked + label:before {
23
- background: ${ props =>
24
- props . disabled
25
- ? 'var(--disabled-color)'
26
- : props . selectedColor
27
- ? props . selectedColor
28
- : 'var(--primary-color)' } ;
29
- }
35
+ top: 50%;
36
+ left: 50%;
37
+ transform: translate(-50%, -50%);
30
38
` ;
31
39
32
- // NOTE (Will): We don't use a transition because it's too slow and creates flickering when changing buttons.
33
- const StyledLabel = styled . label < {
40
+ function RadioButton ( {
41
+ disabled,
42
+ onClick,
43
+ selected,
44
+ dataTestId,
45
+ diameterRadioBg,
46
+ diameterRadioBorder,
47
+ style,
48
+ ariaLabel,
49
+ } : {
50
+ selected : boolean ;
51
+ onClick : MouseEventHandler < HTMLDivElement > ;
34
52
disabled : boolean ;
35
- filledSize : number ;
36
- outlineOffset : number ;
37
- beforeMargins ?: string ;
53
+ dataTestId : SessionDataTestId | undefined ;
54
+ diameterRadioBg : number ;
55
+ diameterRadioBorder : number ;
56
+ style ?: CSSProperties ;
57
+ ariaLabel ?: string ;
58
+ } ) {
59
+ // clickHandler is on the parent button, so we need to skip this input while pressing Tab
60
+ return (
61
+ < StyledRadioOuter
62
+ onClick = { onClick }
63
+ $disabled = { disabled }
64
+ tabIndex = { - 1 }
65
+ data-testid = { dataTestId }
66
+ $diameterRadioBorder = { diameterRadioBorder }
67
+ style = { style }
68
+ aria-label = { ariaLabel }
69
+ >
70
+ < StyledSelectedInner
71
+ $disabled = { disabled }
72
+ $selected = { selected }
73
+ $diameterRadioBg = { diameterRadioBg }
74
+ />
75
+ </ StyledRadioOuter >
76
+ ) ;
77
+ }
78
+
79
+ // NOTE (): We don't use a transition because it's too slow and creates flickering when changing buttons.
80
+ const StyledLabel = styled . label < {
81
+ $disabled : boolean ;
38
82
} > `
39
83
cursor: pointer;
40
- color: ${ props => ( props . disabled ? 'var(--disabled-color)' : 'var(--text-primary-color)' ) } ;
41
-
42
- &:before {
43
- content: '';
44
- display: inline-block;
45
- border-radius: 100%;
46
-
47
- padding: ${ props => props . filledSize } px;
48
- border: none;
49
- outline: 1px solid currentColor; /* CSS variables don't work here */
50
- outline-offset: ${ props => props . outlineOffset } px;
51
- ${ props => props . beforeMargins && `margin: ${ props . beforeMargins } ;` } ;
52
- }
84
+ color: ${ props => ( props . $disabled ? 'var(--disabled-color)' : 'var(--text-primary-color)' ) } ;
85
+ margin-inline-end: var(--margins-sm);
53
86
` ;
54
87
55
88
type SessionRadioProps = {
56
- label : string ;
89
+ label ? : string ;
57
90
value : string ;
58
91
active : boolean ;
59
92
inputName ?: string ;
60
- beforeMargins ?: string ;
61
93
onClick ?: ( value : string ) => void ;
62
94
disabled ?: boolean ;
63
- radioPosition ?: 'left' | 'right' ;
64
95
style ?: CSSProperties ;
65
96
labelDataTestId ?: SessionDataTestId ;
66
97
inputDataTestId ?: SessionDataTestId ;
@@ -69,31 +100,28 @@ type SessionRadioProps = {
69
100
export const SessionRadio = ( props : SessionRadioProps ) => {
70
101
const {
71
102
label,
72
- inputName,
73
103
value,
74
104
active,
75
105
onClick,
76
- beforeMargins,
77
106
disabled = false ,
78
- radioPosition = 'left' ,
79
107
style,
80
108
labelDataTestId,
81
109
inputDataTestId,
82
110
} = props ;
83
111
84
- const clickHandler = ( e : SyntheticEvent < any > ) => {
112
+ const clickHandler = ( e : React . MouseEvent < any > | React . KeyboardEvent < any > ) => {
85
113
if ( ! disabled && onClick ) {
86
114
// let something else catch the event if our click handler is not set
87
115
e . stopPropagation ( ) ;
88
116
onClick ( value ) ;
89
117
}
90
118
} ;
91
119
92
- const filledSize = 15 / 2 ;
93
- const outlineOffset = 2 ;
120
+ const diameterRadioBorder = 26 ;
121
+ const diameterRadioBg = 20 ;
94
122
95
123
return (
96
- < StyledButton
124
+ < StyledContainer
97
125
onKeyDown = { e => {
98
126
if ( e . code === 'Space' ) {
99
127
clickHandler ( e ) ;
@@ -104,112 +132,77 @@ export const SessionRadio = (props: SessionRadioProps) => {
104
132
>
105
133
< Flex
106
134
$container = { true }
107
- $flexDirection = { radioPosition === 'left' ? ' row' : 'row-reverse '}
108
- $justifyContent = { radioPosition === 'left' ? ' flex-start' : 'flex-end '}
109
- style = { { ...style , position : 'relative ' } }
135
+ $flexDirection = { ' row'}
136
+ $justifyContent = { ' flex-start'}
137
+ style = { { ...style , alignItems : 'center' , justifyContent : 'space-between ' } }
110
138
>
111
- < StyledInput
112
- type = "radio"
113
- name = { inputName || '' }
114
- value = { value }
115
- aria-checked = { active }
116
- checked = { active }
117
- onChange = { clickHandler }
118
- tabIndex = { - 1 } // clickHandler is on the parent button, so we need to skip this input while pressing Tab
119
- filledSize = { filledSize * 2 }
120
- outlineOffset = { outlineOffset }
121
- disabled = { disabled }
122
- data-testid = { inputDataTestId }
123
- />
124
- < StyledLabel
125
- role = "button"
139
+ { label ? (
140
+ < StyledLabel
141
+ role = "button"
142
+ onClick = { clickHandler }
143
+ aria-label = { label }
144
+ $disabled = { disabled }
145
+ data-testid = { labelDataTestId }
146
+ >
147
+ { label }
148
+ </ StyledLabel >
149
+ ) : null }
150
+
151
+ < RadioButton
152
+ selected = { active }
126
153
onClick = { clickHandler }
127
- filledSize = { filledSize - 1 }
128
- outlineOffset = { outlineOffset }
129
- beforeMargins = { beforeMargins }
130
- aria-label = { label }
131
154
disabled = { disabled }
132
- data-testid = { labelDataTestId }
133
- >
134
- { label }
135
- </ StyledLabel >
155
+ dataTestId = { inputDataTestId }
156
+ diameterRadioBorder = { diameterRadioBorder }
157
+ diameterRadioBg = { diameterRadioBg }
158
+ / >
136
159
</ Flex >
137
- </ StyledButton >
160
+ </ StyledContainer >
138
161
) ;
139
162
} ;
140
163
141
- const StyledInputOutlineSelected = styled ( StyledInput ) `
142
- color: ${ props => ( props . disabled ? 'var(--disabled-color)' : 'var(--text-primary-color)' ) } ;
143
-
144
- label:before,
145
- label:before {
146
- outline: none;
147
- }
148
-
149
- &:checked + label:before {
150
- outline: 1px solid currentColor;
151
- }
152
- ` ;
153
- const StyledLabelOutlineSelected = styled ( StyledLabel ) < { selectedColor : string } > `
154
- &:before {
155
- background: ${ props =>
156
- props . disabled
157
- ? 'var(--disabled-color)'
158
- : props . selectedColor
159
- ? props . selectedColor
160
- : 'var(--primary-color)' } ;
161
- outline: 1px solid transparent; /* CSS variables don't work here */
162
- }
163
- ` ;
164
-
165
164
/**
166
- * Keeping this component here so we can reuse the `StyledInput` and `StyledLabel` defined locally rather than exporting them
165
+ * This is slightly different that the classic SessionRadio as this one has
166
+ * - no padding between the selected background and the border,
167
+ * - they all have a background color (even when not selected), but the border is present on the selected one
168
+ *
169
+ * Keeping it here so we don't have to export
167
170
*/
168
171
export const SessionRadioPrimaryColors = ( props : {
169
172
value : string ;
170
173
active : boolean ;
171
- inputName ?: string ;
172
174
onClick : ( value : string ) => void ;
173
- ariaLabel ? : string ;
175
+ ariaLabel : string ;
174
176
color : string ; // by default, we use the theme accent color but for the settings screen we need to be able to force it
175
- disabled ?: boolean ;
176
177
} ) => {
177
- const { inputName , value, active, onClick, color, ariaLabel, disabled = false } = props ;
178
+ const { value, active, onClick, color, ariaLabel } = props ;
178
179
179
180
function clickHandler ( e : ChangeEvent < any > ) {
180
181
e . stopPropagation ( ) ;
181
182
onClick ( value ) ;
182
183
}
183
184
184
- const filledSize = 31 / 2 ;
185
- const outlineOffset = 5 ;
185
+ // this component has no padding between the selected background and the border
186
+ const diameterRadioBorder = 26 ;
187
+ const diameterRadioBg = 22 ;
188
+
189
+ const overriddenColorsVars = {
190
+ '--primary-color' : color ,
191
+ '--text-primary-color' : active ? undefined : 'transparent' ,
192
+ } as React . CSSProperties ;
186
193
187
194
return (
188
195
< Flex $container = { true } padding = "0 0 5px 0" >
189
- < StyledInputOutlineSelected
190
- type = "radio"
191
- name = { inputName || '' }
192
- value = { value }
193
- aria-checked = { active }
194
- checked = { active }
195
- onChange = { clickHandler }
196
- filledSize = { filledSize }
197
- outlineOffset = { outlineOffset }
198
- selectedColor = { color }
199
- aria-label = { ariaLabel }
200
- disabled = { disabled }
201
- />
202
-
203
- < StyledLabelOutlineSelected
204
- role = "button"
196
+ < RadioButton
197
+ selected = { true }
205
198
onClick = { clickHandler }
206
- selectedColor = { color }
207
- filledSize = { filledSize }
208
- outlineOffset = { outlineOffset }
209
- disabled = { disabled }
210
- >
211
- { '' }
212
- </ StyledLabelOutlineSelected >
199
+ disabled = { false }
200
+ dataTestId = { undefined }
201
+ diameterRadioBorder = { diameterRadioBorder }
202
+ diameterRadioBg = { diameterRadioBg }
203
+ style = { overriddenColorsVars }
204
+ ariaLabel = { ariaLabel }
205
+ / >
213
206
</ Flex >
214
207
) ;
215
208
} ;
0 commit comments