@@ -31,10 +31,12 @@ const dLoading = 'loading'
3131const dLoadingSpinner = 'loading-spinner'
3232
3333type BaseButtonProps = {
34+ /** Syntactic sugar for setting variant and color together. Will be overridden by explicit variant & color props. */
35+ type ?: 'primary' | 'default' | 'dashed' | 'link' | 'text'
3436 /** Button color */
3537 color ?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'neutral'
3638 /** Button style variant */
37- variant ?: 'solid' | 'outline' | 'dash' | 'soft' | 'ghost' | 'link'
39+ variant ?: 'solid' | 'outline' | 'dash' | 'soft' | 'ghost' | 'link' | 'text'
3840 /** Button size */
3941 size ?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
4042 /** Active/pressed visual state */
@@ -43,6 +45,10 @@ type BaseButtonProps = {
4345 loading ?: boolean
4446 /** Button shape */
4547 shape ?: 'square' | 'circle' | 'wide' | 'block' | 'round'
48+ /** Make the button full width */
49+ block ?: boolean
50+ /** Make background transparent and invert text and border colors */
51+ ghost ?: boolean
4652 /** Disable click animation */
4753 noAnimation ?: boolean
4854 /** Icon element to display */
@@ -55,6 +61,8 @@ type BaseButtonProps = {
5561 danger ?: boolean
5662 /** Toggle button pressed state (sets aria-pressed) */
5763 pressed ?: boolean
64+ /** Accessible label for icon-only buttons */
65+ 'aria-label' ?: string
5866 /** Test ID for testing */
5967 'data-testid' ?: string
6068}
@@ -79,30 +87,62 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
7987 (
8088 {
8189 children,
90+ type,
8291 color,
8392 variant,
8493 size,
8594 active = false ,
8695 loading = false ,
8796 shape,
97+ block = false ,
98+ ghost = false ,
8899 noAnimation = false ,
89100 icon,
90101 iconPlacement,
91102 iconPosition,
92103 danger = false ,
93104 pressed,
94105 className = '' ,
106+ 'aria-label' : ariaLabel ,
95107 'data-testid' : testId ,
96108 ...props
97109 } ,
98110 ref
99111 ) => {
100112 const { componentSize } = useConfig ( )
101113 const effectiveSize = size ?? componentSize ?? 'md'
114+
115+ // Apply type prop (syntactic sugar) - explicit variant/color takes precedence
116+ let derivedVariant = variant
117+ let derivedColor = color
118+ if ( type && ! variant && ! color ) {
119+ switch ( type ) {
120+ case 'primary' :
121+ derivedVariant = 'solid'
122+ derivedColor = 'primary'
123+ break
124+ case 'default' :
125+ derivedVariant = 'outline'
126+ break
127+ case 'dashed' :
128+ derivedVariant = 'dash'
129+ break
130+ case 'link' :
131+ derivedVariant = 'link'
132+ break
133+ case 'text' :
134+ derivedVariant = 'text'
135+ break
136+ }
137+ }
138+
102139 // danger prop is a shorthand for color="error"
103- const effectiveColor = danger ? 'error' : color
140+ const effectiveColor = danger ? 'error' : derivedColor
141+ const effectiveVariant = ghost && ! derivedVariant ? 'ghost' : derivedVariant
104142 // iconPlacement takes precedence over deprecated iconPosition
105143 const effectiveIconPlacement = iconPlacement ?? iconPosition ?? 'start'
144+ // block prop is a shorthand for shape="block"
145+ const effectiveShape = block && ! shape ? 'block' : shape
106146
107147 const colorClasses = {
108148 primary : dBtnPrimary ,
@@ -122,6 +162,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
122162 soft : dBtnSoft ,
123163 ghost : dBtnGhost ,
124164 link : dBtnLink ,
165+ text : dBtnGhost , // text variant uses ghost styling
125166 }
126167
127168 const sizeClasses = {
@@ -143,10 +184,10 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
143184 const classes = [
144185 dBtn ,
145186 effectiveColor && colorClasses [ effectiveColor ] ,
146- variant && variantClasses [ variant ] ,
187+ effectiveVariant && variantClasses [ effectiveVariant ] ,
147188 sizeClasses [ effectiveSize ] ,
148189 active && dBtnActive ,
149- shape && shapeClasses [ shape ] ,
190+ effectiveShape && shapeClasses [ effectiveShape ] ,
150191 noAnimation && 'no-animation' ,
151192 className ,
152193 ]
@@ -215,10 +256,15 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
215256 aria-disabled = { isDisabled || undefined }
216257 aria-busy = { loading || undefined }
217258 aria-pressed = { pressed }
259+ aria-label = { ariaLabel }
218260 tabIndex = { isDisabled ? - 1 : 0 }
219261 onKeyDown = { handleKeyDown }
220262 onClick = { handleClick }
221263 data-testid = { testId }
264+ data-state-loading = { loading || undefined }
265+ data-state-disabled = { isDisabled || undefined }
266+ data-state-active = { active || undefined }
267+ data-state-pressed = { pressed }
222268 { ...anchorProps }
223269 >
224270 { content }
@@ -228,15 +274,21 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
228274
229275 const { htmlType, ...buttonProps } = props as Omit < ButtonAsButton , keyof BaseButtonProps >
230276 const buttonType : 'button' | 'submit' | 'reset' = htmlType ?? 'button'
277+ const isDisabled = loading || buttonProps . disabled
231278 return (
232279 < button
233280 ref = { ref as React . Ref < HTMLButtonElement > }
234281 type = { buttonType }
235282 className = { classes }
236283 aria-busy = { loading || undefined }
237284 aria-pressed = { pressed }
238- disabled = { loading || buttonProps . disabled }
285+ aria-label = { ariaLabel }
286+ disabled = { isDisabled }
239287 data-testid = { testId }
288+ data-state-loading = { loading || undefined }
289+ data-state-disabled = { isDisabled || undefined }
290+ data-state-active = { active || undefined }
291+ data-state-pressed = { pressed }
240292 { ...buttonProps }
241293 >
242294 { content }
0 commit comments