1
1
import isPropValid from '@emotion/is-prop-valid' ;
2
- import type { SerializedStyles , Theme } from '@emotion/react' ;
3
- import { css } from '@emotion/react' ;
4
2
import styled from '@emotion/styled' ;
5
3
6
- import { type LinkButtonProps } from 'sentry/components/core/button/linkButton' ;
7
- import { Tooltip , type TooltipProps } from 'sentry/components/core/tooltip' ;
4
+ import { Tooltip } from 'sentry/components/core/tooltip' ;
8
5
import InteractionStateLayer from 'sentry/components/interactionStateLayer' ;
9
- import type { SVGIconProps } from 'sentry/icons/svgIcon' ;
10
6
import { IconDefaultsProvider } from 'sentry/icons/useIconDefaults' ;
11
- import HookStore from 'sentry/stores/hookStore' ;
12
7
import { space } from 'sentry/styles/space' ;
13
8
14
- import { getChonkButtonStyles } from './index.chonk' ;
9
+ import { DO_NOT_USE_BUTTON_ICON_SIZES , DO_NOT_USE_getButtonStyles } from './styles' ;
10
+ import { DO_NOT_USE_getChonkButtonStyles } from './styles.chonk' ;
11
+ import type { DO_NOT_USE_CommonButtonProps } from './types' ;
12
+ import { useButtonFunctionality } from './useButtonFunctionality' ;
15
13
16
- // We do not want people using this type as it should only be used
17
- // internally by the different button implementations
18
- // eslint-disable-next-line @typescript-eslint/naming-convention
19
- export interface DO_NOT_USE_CommonButtonProps {
20
- /**
21
- * Used when you want to overwrite the default Reload event key for analytics
22
- */
23
- analyticsEventKey ?: string ;
24
- /**
25
- * Used when you want to send an Amplitude Event. By default, Amplitude events are not sent so
26
- * you must pass in a eventName to send an Amplitude event.
27
- */
28
- analyticsEventName ?: string ;
29
- /**
30
- * Adds extra parameters to the analytics tracking
31
- */
32
- analyticsParams ?: Record < string , any > ;
33
- /**
34
- * Removes borders from the button.
35
- */
36
- borderless ?: boolean ;
37
- /**
38
- * Indicates that the button is "doing" something.
39
- */
40
- busy ?: boolean ;
41
- /**
42
- * The icon to render inside of the button. The size will be set
43
- * appropriately based on the size of the button.
44
- */
45
- icon ?: React . ReactNode ;
46
- /**
47
- * The semantic "priority" of the button. Use `primary` when the action is
48
- * contextually the primary action, `danger` if the button will do something
49
- * destructive, `link` for visual similarity to a link.
50
- */
51
- priority ?: 'default' | 'primary' | 'danger' | 'link' ;
52
- /**
53
- * The size of the button
54
- */
55
- size ?: 'zero' | 'xs' | 'sm' | 'md' ;
56
- /**
57
- * Display a tooltip for the button.
58
- */
59
- title ?: TooltipProps [ 'title' ] ;
60
- /**
61
- * Additional properites for the Tooltip when `title` is set.
62
- */
63
- tooltipProps ?: Omit < TooltipProps , 'children' | 'title' | 'skipWrapper' > ;
64
- /**
65
- * Userful in scenarios where the border of the button should blend with the
66
- * background behind the button.
67
- */
68
- translucentBorder ?: boolean ;
69
- }
70
-
71
- /**
72
- * Helper type to extraxct the HTML element props for use in button prop
73
- * interfaces.
74
- *
75
- * XXX(epurkhiser): Right now all usages of this use ButtonElement, but in the
76
- * future ButtonElement should go away and be replaced with HTMLButtonElement
77
- * and HTMLAnchorElement respectively
78
- */
79
14
type ButtonElementProps = Omit <
80
15
React . ButtonHTMLAttributes < HTMLButtonElement > ,
81
16
'label' | 'size' | 'title'
@@ -149,272 +84,27 @@ export function Button({
149
84
export const StyledButton = styled ( 'button' ) < ButtonProps > `
150
85
${ p =>
151
86
p . theme . isChonk
152
- ? getChonkButtonStyles ( p as any )
87
+ ? DO_NOT_USE_getChonkButtonStyles ( p as any )
153
88
: DO_NOT_USE_getButtonStyles ( p as any ) }
154
89
` ;
155
90
156
- export const useButtonFunctionality = ( props : ButtonProps | LinkButtonProps ) => {
157
- // Fallbacking aria-label to string children is not necessary as screen
158
- // readers natively understand that scenario. Leaving it here for a bunch of
159
- // our tests that query by aria-label.
160
- const accessibleLabel =
161
- props [ 'aria-label' ] ??
162
- ( typeof props . children === 'string' ? props . children : undefined ) ;
163
-
164
- const useButtonTrackingLogger = ( ) => {
165
- const hasAnalyticsDebug = window . localStorage ?. getItem ( 'DEBUG_ANALYTICS' ) === '1' ;
166
- const hasCustomAnalytics =
167
- props . analyticsEventName || props . analyticsEventKey || props . analyticsParams ;
168
- if ( ! hasCustomAnalytics || ! hasAnalyticsDebug ) {
169
- return ( ) => { } ;
170
- }
171
-
172
- return ( ) => {
173
- // eslint-disable-next-line no-console
174
- console . log ( 'buttonAnalyticsEvent' , {
175
- eventKey : props . analyticsEventKey ,
176
- eventName : props . analyticsEventName ,
177
- priority : props . priority ,
178
- href : 'href' in props ? props . href : undefined ,
179
- ...props . analyticsParams ,
180
- } ) ;
181
- } ;
182
- } ;
183
-
184
- const useButtonTracking =
185
- HookStore . get ( 'react-hook:use-button-tracking' ) [ 0 ] ?? useButtonTrackingLogger ;
186
-
187
- const buttonTracking = useButtonTracking ( {
188
- analyticsEventName : props . analyticsEventName ,
189
- analyticsEventKey : props . analyticsEventKey ,
190
- analyticsParams : {
191
- priority : props . priority ,
192
- href : 'href' in props ? props . href : undefined ,
193
- ...props . analyticsParams ,
194
- } ,
195
- 'aria-label' : accessibleLabel || '' ,
196
- } ) ;
197
-
198
- const handleClick = ( e : React . MouseEvent < HTMLButtonElement | HTMLAnchorElement > ) => {
199
- // Don't allow clicks when disabled or busy
200
- if ( props . disabled || props . busy ) {
201
- e . preventDefault ( ) ;
202
- e . stopPropagation ( ) ;
203
- return ;
204
- }
205
-
206
- buttonTracking ( ) ;
207
- // @ts -expect-error at this point, we don't know if the button is a button or a link
208
- props . onClick ?.( e ) ;
209
- } ;
210
-
211
- const hasChildren = Array . isArray ( props . children )
212
- ? props . children . some ( child => ! ! child || String ( child ) === '0' )
213
- : ! ! props . children || String ( props . children ) === '0' ;
214
-
215
- // Buttons come in 4 flavors: <Link>, <ExternalLink>, <a>, and <button>.
216
- // Let's use props to determine which to serve up, so we don't have to think about it.
217
- // *Note* you must still handle tabindex manually.
218
-
219
- return {
220
- handleClick,
221
- hasChildren,
222
- accessibleLabel,
223
- } ;
224
- } ;
225
-
226
- const getBoxShadow = ( {
227
- priority,
228
- borderless,
229
- translucentBorder,
230
- disabled,
231
- size,
232
- theme,
233
- } : ( ButtonProps | LinkButtonProps ) & { theme : Theme } ) : SerializedStyles => {
234
- if ( disabled || borderless || priority === 'link' ) {
235
- return css `
236
- box-shadow : none;
237
- ` ;
238
- }
239
-
240
- const themeName = disabled ? 'disabled' : priority || 'default' ;
241
- const { borderTranslucent} = theme . button [ themeName ] ;
242
- const translucentBorderString = translucentBorder
243
- ? `0 0 0 1px ${ borderTranslucent } ,`
244
- : '' ;
245
- const dropShadow = size === 'xs' ? theme . dropShadowLight : theme . dropShadowMedium ;
246
-
247
- return css `
248
- box-shadow : ${ translucentBorderString } ${ dropShadow } ;
249
- & : active {
250
- box-shadow : ${ translucentBorderString } inset ${ dropShadow } ;
251
- }
252
- ` ;
253
- } ;
254
-
255
- const getColors = ( {
256
- size,
257
- priority,
258
- disabled,
259
- borderless,
260
- translucentBorder,
261
- theme,
262
- } : ( ButtonProps | LinkButtonProps ) & { theme : Theme } ) : SerializedStyles => {
263
- const themeName = disabled ? 'disabled' : priority || 'default' ;
264
- const { color, colorActive, background, border, borderActive, focusBorder, focusShadow} =
265
- theme . button [ themeName ] ;
266
-
267
- const getFocusState = ( ) : SerializedStyles => {
268
- switch ( priority ) {
269
- case 'primary' :
270
- case 'danger' :
271
- return css `
272
- border-color : ${ focusBorder } ;
273
- box-shadow :
274
- ${ focusBorder } 0 0 0 1px ,
275
- ${ focusShadow } 0 0 0 4px ;
276
- ` ;
277
- default :
278
- if ( translucentBorder ) {
279
- return css `
280
- border-color : ${ focusBorder } ;
281
- box-shadow : ${ focusBorder } 0 0 0 2px ;
282
- ` ;
283
- }
284
- return css `
285
- border-color : ${ focusBorder } ;
286
- box-shadow : ${ focusBorder } 0 0 0 1px ;
287
- ` ;
288
- }
289
- } ;
290
-
291
- return css `
292
- color : ${ color } ;
293
- background-color : ${ priority === 'primary' || priority === 'danger'
294
- ? background
295
- : borderless
296
- ? 'transparent'
297
- : background } ;
298
-
299
- border : 1px solid ${ borderless || priority === 'link' ? 'transparent' : border } ;
300
-
301
- ${ translucentBorder &&
302
- css `
303
- border-width : 0 ;
304
- ` }
305
-
306
- & : hover {
307
- color : ${ color } ;
308
- }
309
-
310
- ${ size !== 'zero' &&
311
- css `
312
- & : hover,
313
- & : active,
314
- & [aria-expanded = 'true' ] {
315
- color : ${ colorActive || color } ;
316
- border-color : ${ borderless || priority === 'link' ? 'transparent' : borderActive } ;
317
- }
318
-
319
- & : focus-visible {
320
- color : ${ colorActive || color } ;
321
- border-color : ${ borderActive } ;
322
- }
323
- ` }
324
-
325
- & : focus-visible {
326
- ${ getFocusState ( ) }
327
- z-index : 1 ;
328
- }
329
- ` ;
330
- } ;
331
-
332
- const getSizeStyles = ( {
333
- size = 'md' ,
334
- translucentBorder,
335
- theme,
336
- } : ( ButtonProps | LinkButtonProps ) & { theme : Theme } ) : SerializedStyles => {
337
- const buttonSize = size === 'zero' ? 'md' : size ;
338
- const formStyles = theme . form [ buttonSize ] ;
339
- const buttonPadding = theme . buttonPadding [ buttonSize ] ;
340
-
341
- // If using translucent borders, rewrite size styles to
342
- // prevent layout shifts
343
- const borderStyles = translucentBorder
344
- ? {
345
- height : `calc(${ formStyles . height } - 2px)` ,
346
- minHeight : `calc(${ formStyles . minHeight } - 2px)` ,
347
- paddingTop : buttonPadding . paddingTop - 1 ,
348
- paddingBottom : buttonPadding . paddingBottom - 1 ,
349
- margin : 1 ,
350
- }
351
- : { } ;
352
-
353
- return css `
354
- ${ formStyles }
355
- ${ buttonPadding }
356
- ${ borderStyles }
357
- ` ;
358
- } ;
359
-
360
- // This should only be used by the different underlying button implementations
361
- // and not directly by consumers of the button component.
362
- export function DO_NOT_USE_getButtonStyles (
363
- p : ( ButtonProps | LinkButtonProps ) & { theme : Theme }
364
- ) : SerializedStyles {
365
- return css `
366
- position : relative;
367
- display : inline-block;
368
- border-radius : ${ p . theme . borderRadius } ;
369
- text-transform : none;
370
- font-weight : ${ p . theme . fontWeightBold } ;
371
- cursor : ${ p . disabled ? 'not-allowed' : 'pointer' } ;
372
- opacity : ${ ( p . busy || p . disabled ) && '0.65' } ;
373
-
374
- ${ getColors ( p ) }
375
- ${ getSizeStyles ( p ) }
376
- ${ getBoxShadow ( p ) }
377
-
378
- transition :
379
- background 0.1s ,
380
- border 0.1s ,
381
- box-shadow 0.1s ;
382
-
383
- ${ p . priority === 'link' &&
384
- css `
385
- font-size : inherit;
386
- font-weight : inherit;
387
- padding : 0 ;
388
- height : auto;
389
- min-height : auto;
390
- ` }
391
- ${ p . size === 'zero' &&
392
- css `
393
- height : auto;
394
- min-height : auto;
395
- padding : ${ space ( 0.25 ) } ;
396
- ` }
397
-
398
- & : focus {
399
- outline : none;
400
- }
401
- ` ;
402
- }
403
-
404
91
const ButtonLabel = styled ( 'span' , {
405
92
shouldForwardProp : prop =>
406
93
typeof prop === 'string' &&
407
94
isPropValid ( prop ) &&
408
95
! [ 'size' , 'borderless' ] . includes ( prop ) ,
409
- } ) < Pick < ButtonProps , 'size' | 'borderless' > > `
96
+ } ) < Pick < DO_NOT_USE_CommonButtonProps , 'size' | 'borderless' > > `
410
97
height: 100%;
411
98
display: flex;
412
99
align-items: center;
413
100
justify-content: center;
414
101
white-space: nowrap;
415
102
` ;
416
103
417
- const Icon = styled ( 'span' ) < { hasChildren ?: boolean ; size ?: ButtonProps [ 'size' ] } > `
104
+ const Icon = styled ( 'span' ) < {
105
+ hasChildren ?: boolean ;
106
+ size ?: DO_NOT_USE_CommonButtonProps [ 'size' ] ;
107
+ } > `
418
108
display: flex;
419
109
align-items: center;
420
110
margin-right: ${ p =>
@@ -425,13 +115,3 @@ const Icon = styled('span')<{hasChildren?: boolean; size?: ButtonProps['size']}>
425
115
: '0' } ;
426
116
flex-shrink: 0;
427
117
` ;
428
-
429
- export const DO_NOT_USE_BUTTON_ICON_SIZES : Record <
430
- NonNullable < BaseButtonProps [ 'size' ] > ,
431
- SVGIconProps [ 'size' ]
432
- > = {
433
- zero : undefined ,
434
- xs : 'xs' ,
435
- sm : 'sm' ,
436
- md : 'sm' ,
437
- } ;
0 commit comments