11import isPropValid from '@emotion/is-prop-valid' ;
2- import type { SerializedStyles , Theme } from '@emotion/react' ;
3- import { css } from '@emotion/react' ;
42import styled from '@emotion/styled' ;
53
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' ;
85import InteractionStateLayer from 'sentry/components/interactionStateLayer' ;
9- import type { SVGIconProps } from 'sentry/icons/svgIcon' ;
106import { IconDefaultsProvider } from 'sentry/icons/useIconDefaults' ;
11- import HookStore from 'sentry/stores/hookStore' ;
127import { space } from 'sentry/styles/space' ;
138
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' ;
1513
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- */
7914type ButtonElementProps = Omit <
8015 React . ButtonHTMLAttributes < HTMLButtonElement > ,
8116 'label' | 'size' | 'title'
@@ -149,272 +84,27 @@ export function Button({
14984export const StyledButton = styled ( 'button' ) < ButtonProps > `
15085 ${ p =>
15186 p . theme . isChonk
152- ? getChonkButtonStyles ( p as any )
87+ ? DO_NOT_USE_getChonkButtonStyles ( p as any )
15388 : DO_NOT_USE_getButtonStyles ( p as any ) }
15489` ;
15590
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-
40491const ButtonLabel = styled ( 'span' , {
40592 shouldForwardProp : prop =>
40693 typeof prop === 'string' &&
40794 isPropValid ( prop ) &&
40895 ! [ 'size' , 'borderless' ] . includes ( prop ) ,
409- } ) < Pick < ButtonProps , 'size' | 'borderless' > > `
96+ } ) < Pick < DO_NOT_USE_CommonButtonProps , 'size' | 'borderless' > > `
41097 height: 100%;
41198 display: flex;
41299 align-items: center;
413100 justify-content: center;
414101 white-space: nowrap;
415102` ;
416103
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+ } > `
418108 display: flex;
419109 align-items: center;
420110 margin-right: ${ p =>
@@ -425,13 +115,3 @@ const Icon = styled('span')<{hasChildren?: boolean; size?: ButtonProps['size']}>
425115 : '0' } ;
426116 flex-shrink: 0;
427117` ;
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