11import { FocusableRef } from '@react-types/shared' ;
2- import { cloneElement , forwardRef , ReactElement , useMemo } from 'react' ;
2+ import {
3+ forwardRef ,
4+ HTMLAttributes ,
5+ ReactElement ,
6+ ReactNode ,
7+ RefObject ,
8+ useMemo ,
9+ } from 'react' ;
10+ import { OverlayProps } from 'react-aria' ;
311
412import { useWarn } from '../../../_internal/hooks/use-warn' ;
513import {
@@ -32,10 +40,13 @@ import { LoadingIcon } from '../../../icons';
3240import {
3341 CONTAINER_STYLES ,
3442 extractStyles ,
43+ Styles ,
3544 tasty ,
3645 TEXT_STYLES ,
3746} from '../../../tasty' ;
38- import { Text } from '../../content/Text' ;
47+ import { mergeProps } from '../../../utils/react' ;
48+ import { useAutoTooltip } from '../../content/use-auto-tooltip' ;
49+ import { CubeTooltipProviderProps } from '../../overlays/Tooltip/TooltipProvider' ;
3950import { CubeActionProps } from '../Action/Action' ;
4051import { useAction } from '../use-action' ;
4152
@@ -54,6 +65,22 @@ export interface CubeButtonProps extends CubeActionProps {
5465 | 'neutral'
5566 | ( string & { } ) ;
5667 size ?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | ( string & { } ) ;
68+ /**
69+ * Tooltip content and configuration:
70+ * - string: simple tooltip text
71+ * - true: auto tooltip on overflow (shows children as tooltip when truncated)
72+ * - object: advanced configuration with optional auto property
73+ */
74+ tooltip ?:
75+ | string
76+ | boolean
77+ | ( Omit < CubeTooltipProviderProps , 'children' > & { auto ?: boolean } ) ;
78+ /**
79+ * @private
80+ * Default tooltip placement for the button.
81+ * @default "top"
82+ */
83+ defaultTooltipPlacement ?: OverlayProps [ 'placement' ] ;
5784}
5885
5986export type ButtonVariant =
@@ -84,18 +111,28 @@ export type ButtonVariant =
84111
85112const STYLE_PROPS = [ ...CONTAINER_STYLES , ...TEXT_STYLES ] ;
86113
114+ const DEFAULT_ICON_STYLES : Styles = {
115+ $ : '>' ,
116+ display : 'grid' ,
117+ placeItems : 'center' ,
118+ placeContent : 'stretch' ,
119+ aspectRatio : '1 / 1' ,
120+ width : '($size - 2bw)' ,
121+ } ;
122+
87123export const DEFAULT_BUTTON_STYLES = {
88124 display : 'inline-grid' ,
89- flow : 'column' ,
90- placeItems : 'center start' ,
91- placeContent : {
92- '' : 'center ' ,
93- 'right-icon | suffix ' : 'center stretch ' ,
125+ flow : 'column dense ' ,
126+ gap : 0 ,
127+ gridTemplate : {
128+ '' : '"icon label rightIcon" auto / max-content 1sf max-content ' ,
129+ 'raw-children ' : 'initial ' ,
94130 } ,
95- gridColumns : {
96- '' : 'initial ' ,
97- 'left-icon | loading | prefix ' : 'max-content ' ,
131+ placeItems : {
132+ '' : 'stretch ' ,
133+ 'raw-children ' : 'center stretch ' ,
98134 } ,
135+ placeContent : 'center stretch' ,
99136 position : 'relative' ,
100137 margin : 0 ,
101138 boxSizing : 'border-box' ,
@@ -105,10 +142,6 @@ export const DEFAULT_BUTTON_STYLES = {
105142 ':is(button)' : '$pointer' ,
106143 disabled : 'not-allowed' ,
107144 } ,
108- gap : {
109- '' : '.75x' ,
110- 'size=small' : '.5x' ,
111- } ,
112145 preset : {
113146 '' : 't3m' ,
114147 'size=xsmall' : 't4' ,
@@ -120,16 +153,14 @@ export const DEFAULT_BUTTON_STYLES = {
120153 outline : 0 ,
121154 outlineOffset : 1 ,
122155 padding : {
123- '' : '.5x (1.5x - 1bw)' ,
124- 'size=small | size=xsmall' : '.5x (1.25x - 1bw)' ,
125- 'size=medium' : '.5x (1.5x - 1bw)' ,
126- 'size=large' : '.5x (1.75x - 1bw)' ,
127- 'size=xlarge' : '.5x (2x - 1bw)' ,
128- 'single-icon | type=link' : 0 ,
156+ '' : 0 ,
157+ 'raw-children' :
158+ '$block-padding $label-padding-right $block-padding $label-padding-left' ,
159+ 'raw-children & type=link' : 0 ,
129160 } ,
130161 width : {
131162 '' : 'min $size' ,
132- 'left -icon & right-icon' : 'min ($size * 2 - 2bw)' ,
163+ 'has -icon & has- right-icon' : 'min ($size * 2 - 2bw)' ,
133164 'single-icon' : 'fixed $size' ,
134165 'type=link' : 'min 1ch' ,
135166 } ,
@@ -151,19 +182,57 @@ export const DEFAULT_BUTTON_STYLES = {
151182 'size=large' : '$size-lg' ,
152183 'size=xlarge' : '$size-xl' ,
153184 } ,
185+ '$inline-padding' : {
186+ '' : 'max($min-inline-padding, (($size - 1lh - 2bw) / 2 + $inline-compensation))' ,
187+ } ,
188+ '$block-padding' : {
189+ '' : '.5x' ,
190+ 'size=xsmall | size=small' : '.25x' ,
191+ } ,
192+ '$inline-compensation' : '.5x' ,
193+ '$min-inline-padding' : '(1x - 1bw)' ,
194+ '$label-padding-left' : {
195+ '' : '$inline-padding' ,
196+ 'has-icon' : 0 ,
197+ } ,
198+ '$label-padding-right' : {
199+ '' : '$inline-padding' ,
200+ 'has-right-icon' : 0 ,
201+ } ,
154202
155- ButtonIcon : {
156- width : 'min 1fs' ,
203+ // Icon sub-element (recommended format)
204+ Icon : {
205+ ...DEFAULT_ICON_STYLES ,
206+ gridArea : 'icon' ,
157207 } ,
158208
159- '& [data-element="ButtonIcon"]:first-child:not(:last-child)' : {
160- marginLeft : '-.5x' ,
161- placeSelf : 'center start' ,
209+ // RightIcon sub-element (recommended format)
210+ RightIcon : {
211+ ...DEFAULT_ICON_STYLES ,
212+ gridArea : 'rightIcon' ,
162213 } ,
163214
164- '& [data-element="ButtonIcon"]:last-child:not(:first-child)' : {
165- marginRight : '-.5x' ,
166- placeSelf : 'center end' ,
215+ // Label sub-element (recommended format)
216+ Label : {
217+ $ : '>' ,
218+ gridArea : 'label' ,
219+ display : 'block' ,
220+ placeSelf : 'center stretch' ,
221+ boxSizing : 'border-box' ,
222+ whiteSpace : 'nowrap' ,
223+ overflow : 'hidden' ,
224+ textOverflow : 'ellipsis' ,
225+ maxWidth : '100%' ,
226+ textAlign : 'center' ,
227+ padding : {
228+ '' : '$block-padding $label-padding-right $block-padding $label-padding-left' ,
229+ 'type=link' : 0 ,
230+ } ,
231+ } ,
232+
233+ // ButtonIcon sub-element (backward compatibility)
234+ ButtonIcon : {
235+ width : 'min 1fs' ,
167236 } ,
168237} as const ;
169238
@@ -219,6 +288,8 @@ export const Button = forwardRef(function Button(
219288 rightIcon,
220289 mods,
221290 download,
291+ tooltip = true ,
292+ defaultTooltipPlacement = 'top' ,
222293 ...props
223294 } = allProps ;
224295
@@ -252,34 +323,30 @@ export const Button = forwardRef(function Button(
252323 label = 'Unnamed' ; // fix to avoid warning in production
253324 }
254325
255- if ( icon ) {
256- icon = cloneElement ( icon , {
257- 'data-element' : 'ButtonIcon' ,
258- } as any ) ;
259- }
260-
261- if ( rightIcon ) {
262- rightIcon = cloneElement ( rightIcon , {
263- 'data-element' : 'ButtonIcon' ,
264- } as any ) ;
265- }
266-
326+ const hasLeftIcon = ! ! icon || isLoading ;
327+ const hasChildren = children != null ;
267328 const singleIcon = ! ! (
268- ( ( icon && ! rightIcon ) || ( rightIcon && ! icon ) ) &&
269- ! children
329+ ( ( hasLeftIcon && ! rightIcon ) || ( rightIcon && ! hasLeftIcon ) ) &&
330+ ! hasChildren
270331 ) ;
271332
272- const hasIcons = ! ! icon || ! ! rightIcon ;
333+ const hasIcons = hasLeftIcon || ! ! rightIcon ;
334+ const rawChildren = ! ! (
335+ hasChildren &&
336+ typeof children !== 'string' &&
337+ ! hasIcons
338+ ) ;
273339
274340 const modifiers = useMemo (
275341 ( ) => ( {
276342 loading : isLoading ,
277343 selected : isSelected ,
278344 'has-icons' : hasIcons ,
279- 'left -icon' : ! ! icon ,
280- 'right-icon' : ! ! rightIcon ,
345+ 'has -icon' : hasLeftIcon ,
346+ 'has- right-icon' : ! ! rightIcon ,
281347 'single-icon' : singleIcon ,
282- 'text-only' : ! ! ( children && typeof children === 'string' && ! hasIcons ) ,
348+ 'text-only' : ! ! ( hasChildren && typeof children === 'string' && ! hasIcons ) ,
349+ 'raw-children' : rawChildren ,
283350 ...mods ,
284351 } ) ,
285352 [
@@ -291,6 +358,7 @@ export const Button = forwardRef(function Button(
291358 isSelected ,
292359 singleIcon ,
293360 hasIcons ,
361+ rawChildren ,
294362 ] ,
295363 ) ;
296364
@@ -304,32 +372,63 @@ export const Button = forwardRef(function Button(
304372
305373 delete actionProps . isDisabled ;
306374
307- return (
308- < ButtonElement
309- download = { download }
310- { ...actionProps }
311- disabled = { isDisabledElement }
312- variant = { `${ theme } .${ type ?? 'outline' } ` as ButtonVariant }
313- data-theme = { theme }
314- data-type = { type ?? 'outline' }
315- data-size = { size ?? 'medium' }
316- styles = { styles }
317- >
318- { icon || isLoading ? (
319- ! isLoading ? (
320- icon
321- ) : (
322- < LoadingIcon data-element = "ButtonIcon" />
323- )
324- ) : null }
325- { typeof children === 'string' ? (
326- < Text ellipsis nowrap >
327- { children }
328- </ Text >
329- ) : (
330- children
331- ) }
332- { rightIcon }
333- </ ButtonElement >
334- ) ;
375+ const {
376+ labelProps : finalLabelProps ,
377+ labelRef,
378+ renderWithTooltip,
379+ } = useAutoTooltip ( {
380+ tooltip,
381+ children,
382+ labelProps : undefined ,
383+ } ) ;
384+
385+ // Render function that creates the button element
386+ const renderButtonElement = (
387+ tooltipTriggerProps ?: HTMLAttributes < HTMLElement > ,
388+ tooltipRef ?: RefObject < HTMLElement > ,
389+ ) : ReactNode => {
390+ // Use callback ref to merge multiple refs without calling hooks
391+ const handleRef = ( element : HTMLElement | null ) => {
392+ // Set the component's forwarded ref from useAction
393+ const domRef = actionProps . ref as any ;
394+ if ( typeof domRef === 'function' ) {
395+ domRef ( element ) ;
396+ } else if ( domRef ) {
397+ domRef . current = element ;
398+ }
399+ // Set the tooltip ref if provided
400+ if ( tooltipRef ) {
401+ ( tooltipRef as any ) . current = element ;
402+ }
403+ } ;
404+
405+ return (
406+ < ButtonElement
407+ download = { download }
408+ { ...mergeProps ( actionProps , tooltipTriggerProps || { } ) }
409+ ref = { handleRef }
410+ disabled = { isDisabledElement }
411+ variant = { `${ theme } .${ type ?? 'outline' } ` as ButtonVariant }
412+ data-theme = { theme }
413+ data-type = { type ?? 'outline' }
414+ data-size = { size ?? 'medium' }
415+ styles = { styles }
416+ >
417+ { ( icon || isLoading ) && (
418+ < div data-element = "Icon" > { isLoading ? < LoadingIcon /> : icon } </ div >
419+ ) }
420+ { hasChildren &&
421+ ( rawChildren ? (
422+ children
423+ ) : (
424+ < div data-element = "Label" { ...finalLabelProps } ref = { labelRef } >
425+ { children }
426+ </ div >
427+ ) ) }
428+ { rightIcon && < div data-element = "RightIcon" > { rightIcon } </ div > }
429+ </ ButtonElement >
430+ ) ;
431+ } ;
432+
433+ return renderWithTooltip ( renderButtonElement , defaultTooltipPlacement ) ;
335434} ) ;
0 commit comments