@@ -2,6 +2,9 @@ import * as React from 'react';
22import { Box } from '@twilio-paste/box' ;
33import { Stack } from '@twilio-paste/stack' ;
44import { Spinner } from '@twilio-paste/spinner' ;
5+ import { secureExternalLink } from '@twilio-paste/anchor' ;
6+ import { useSpring , animated } from '@twilio-paste/animation-library' ;
7+ import { ArrowForwardIcon } from '@twilio-paste/icons/esm/ArrowForwardIcon' ;
58import type {
69 ButtonProps ,
710 ButtonSizes ,
@@ -21,6 +24,8 @@ import {InverseButton} from './InverseButton';
2124import { InverseLinkButton } from './InverseLinkButton' ;
2225import { ResetButton } from './ResetButton' ;
2326
27+ const AnimatedBox = animated ( Box ) ;
28+
2429// If size isn't passed, come up with a smart default:
2530// - 'reset' for variant 'link'
2631// - 'icon' if there's 1 child that's an icon
@@ -29,12 +34,12 @@ const getButtonSize = (variant: ButtonVariants, children: React.ReactNode, size?
2934 let smartSize : ButtonSizes = 'default' ;
3035 if ( size != null ) {
3136 smartSize = size ;
32- } else if ( variant === 'link' || variant === 'destructive_link' ) {
37+ } else if ( variant === 'link' || variant === 'destructive_link' || variant === 'reset' ) {
3338 smartSize = 'reset' ;
3439 } else if ( React . Children . count ( children ) === 1 ) {
3540 React . Children . forEach ( children , ( child ) => {
3641 if ( React . isValidElement ( child ) ) {
37- // @ts -ignore
42+ // @ts -expect-error we know displayName will exist in React
3843 if ( typeof child . type . displayName === 'string' && child . type . displayName . includes ( 'Icon' ) ) {
3944 smartSize = 'icon' ;
4045 }
@@ -54,25 +59,50 @@ const getButtonState = (disabled?: boolean, loading?: boolean): ButtonStates =>
5459 return 'default' ;
5560} ;
5661
57- const handlePropValidation = ( { as, href, tabIndex, variant, size, fullWidth, children} : ButtonProps ) : void => {
62+ const handlePropValidation = ( {
63+ as,
64+ href,
65+ tabIndex,
66+ variant,
67+ size,
68+ fullWidth,
69+ children,
70+ disabled,
71+ loading,
72+ } : ButtonProps ) : void => {
5873 const hasHref = href != null && href !== '' ;
5974 const hasTabIndex = tabIndex != null ;
6075
76+ // Link validation
6177 if ( as !== 'a' && hasHref ) {
6278 throw new Error ( `[Paste: Button] You cannot pass href into a button without the 'a' tag. Use 'as="a"'.` ) ;
6379 }
64- if ( as === 'a' && ! hasHref ) {
65- throw new Error ( `[Paste: Button] Missing href prop for link button.` ) ;
66- }
67- if ( as === 'a' && variant === 'link' ) {
68- throw new Error ( `[Paste: Button] This should be a link. Use the [Paste: Anchor] component.` ) ;
80+ if ( as === 'a' ) {
81+ if ( ! hasHref ) {
82+ throw new Error ( `[Paste: Button] Missing href prop for link button.` ) ;
83+ }
84+ if ( variant === 'link' || variant === 'inverse_link' ) {
85+ throw new Error ( `[Paste: Button] Using Button component as an Anchor. Use the Paste Anchor component instead.` ) ;
86+ }
87+ if ( variant !== 'primary' && variant !== 'secondary' && variant !== 'reset' ) {
88+ throw new Error ( `[Paste: Button] <Button as="a"> only works with the following variants: primary or secondary.` ) ;
89+ }
90+ if ( disabled || loading ) {
91+ throw new Error ( `[Paste: Button] <Button as="a"> cannot be disabled or loading.` ) ;
92+ }
6993 }
94+
95+ // Reset validation
7096 if ( variant === 'reset' && size !== 'reset' ) {
7197 throw new Error ( '[Paste: Button] The "RESET" variant can only be used with the "RESET" size.' ) ;
7298 }
99+
100+ // Icon validation
73101 if ( ( size === 'icon' || size === 'icon_small' ) && fullWidth ) {
74102 throw new Error ( '[Paste: Button] Icon buttons should not be fullWidth.' ) ;
75103 }
104+
105+ // Button validation
76106 if ( children == null ) {
77107 throw new Error ( `[Paste: Button] Must have non-null children.` ) ;
78108 }
@@ -147,20 +177,55 @@ const getButtonComponent = (variant: ButtonVariants): React.FunctionComponent<Di
147177// memo
148178const Button = React . forwardRef < HTMLButtonElement , ButtonProps > ( ( props , ref ) => {
149179 const { size, variant, children, disabled, loading, ...rest } = props ;
180+ const [ hovered , setHovered ] = React . useState ( false ) ;
181+ const arrowIconStyles = useSpring ( {
182+ translateX : hovered ? '4px' : '0px' ,
183+ config : {
184+ mass : 0.1 ,
185+ tension : 275 ,
186+ friction : 16 ,
187+ } ,
188+ } ) ;
189+
190+ const smartDefaultSize = React . useMemo ( ( ) => {
191+ return getButtonSize ( variant , children , size ) ;
192+ } , [ size , variant , children ] ) ;
150193
151- handlePropValidation ( props ) ;
194+ handlePropValidation ( { ... props , size : smartDefaultSize } ) ;
152195
153196 const buttonState = getButtonState ( disabled , loading ) ;
154197 const showLoading = buttonState === 'loading' ;
155198 const showDisabled = buttonState !== 'default' ;
156199 const ButtonComponent = getButtonComponent ( variant ) ;
157- const smartDefaultSize = React . useMemo ( ( ) => {
158- return getButtonSize ( variant , children , size ) ;
159- } , [ size , variant , children ] ) ;
200+ // Automatically inject AnchorForwardIcon for link's dressed as buttons when possible
201+ const injectIconChildren =
202+ props . as === 'a' && props . href != null && typeof children === 'string' && variant !== 'reset' ? (
203+ < >
204+ { children }
205+ < AnimatedBox style = { arrowIconStyles } >
206+ < ArrowForwardIcon decorative />
207+ </ AnimatedBox >
208+ </ >
209+ ) : (
210+ children
211+ ) ;
160212
161213 return (
162214 < ButtonComponent
215+ { ...( rest . href != null ? secureExternalLink ( rest . href ) : null ) }
163216 { ...rest }
217+ onMouseEnter = { ( event ) => {
218+ if ( typeof rest . onMouseEnter === 'function' ) {
219+ rest . onMouseEnter ( event ) ;
220+ }
221+ setHovered ( true ) ;
222+ } }
223+ onMouseLeave = { ( event ) => {
224+ if ( typeof rest . onMouseLeave === 'function' ) {
225+ rest . onMouseLeave ( event ) ;
226+ }
227+ setHovered ( false ) ;
228+ } }
164229 buttonState = { buttonState }
165230 disabled = { showDisabled }
166231 size = { smartDefaultSize as ButtonSizes }
@@ -170,7 +235,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) =>
170235 ref = { ref }
171236 >
172237 < ButtonContents buttonState = { buttonState } showLoading = { showLoading } variant = { variant } >
173- { children }
238+ { injectIconChildren }
174239 </ ButtonContents >
175240 </ ButtonComponent >
176241 ) ;
0 commit comments