Skip to content

Commit 4005d6a

Browse files
authored
Button: enhancements with ButtonLink and IconButton (#1659)
* Create icon only button * Add ButtonLink and IconButton components * Lint and test * Remove react router for now. Maybe add an example later * Fix the typescript error on as prop * Fix lint issue * Fix 1. Bug with disabled 2. Tests in testing lib * lint issues
1 parent 3a2b16a commit 4005d6a

File tree

10 files changed

+760
-432
lines changed

10 files changed

+760
-432
lines changed

src/NewButton/button-base.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, {ComponentPropsWithRef, forwardRef} from 'react'
2+
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
3+
import Box from '../Box'
4+
import {merge, SxProp} from '../sx'
5+
import {useTheme} from '../ThemeProvider'
6+
import {ButtonProps, StyledButton} from './types'
7+
import {getVariantStyles, getSizeStyles, getButtonStyles} from './styles'
8+
9+
const ButtonBase = forwardRef<HTMLElement, ButtonProps>(
10+
({children, as: Component = 'button', sx: sxProp = {}, ...props}, forwardedRef): JSX.Element => {
11+
const {leadingIcon: LeadingIcon, trailingIcon: TrailingIcon, variant = 'default', size = 'medium'} = props
12+
const {theme} = useTheme()
13+
const iconWrapStyles = {
14+
display: 'inline-block'
15+
}
16+
const sxStyles = merge.all([
17+
getButtonStyles(theme),
18+
getSizeStyles(size, variant, false),
19+
getVariantStyles(variant, theme),
20+
sxProp as SxProp
21+
])
22+
return (
23+
<StyledButton as={Component} sx={sxStyles} {...props} ref={forwardedRef}>
24+
{LeadingIcon && (
25+
<Box as="span" data-component="leadingIcon" sx={iconWrapStyles}>
26+
<LeadingIcon />
27+
</Box>
28+
)}
29+
<span data-component="text">{children}</span>
30+
{TrailingIcon && (
31+
<Box as="span" data-component="trailingIcon" sx={{...iconWrapStyles, ml: 2}}>
32+
<TrailingIcon />
33+
</Box>
34+
)}
35+
</StyledButton>
36+
)
37+
}
38+
) as PolymorphicForwardRefComponent<'button' | 'a', ButtonProps>
39+
40+
export type ButtonBaseProps = ComponentPropsWithRef<typeof ButtonBase>
41+
42+
export default ButtonBase

src/NewButton/button-link.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React, {forwardRef} from 'react'
2+
import {LinkButtonProps} from './types'
3+
import ButtonBase, {ButtonBaseProps} from './button-base'
4+
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
5+
6+
type MyProps = LinkButtonProps & ButtonBaseProps
7+
8+
const LinkButton = forwardRef<HTMLElement, MyProps>(
9+
({children, as: Component = 'a', ...props}, forwardedRef): JSX.Element => {
10+
return (
11+
<ButtonBase as={Component} ref={forwardedRef} {...props}>
12+
{children}
13+
</ButtonBase>
14+
)
15+
}
16+
) as PolymorphicForwardRefComponent<'a', ButtonBaseProps>
17+
18+
export default LinkButton

src/NewButton/button.tsx

Lines changed: 9 additions & 277 deletions
Original file line numberDiff line numberDiff line change
@@ -1,283 +1,15 @@
11
import React, {forwardRef} from 'react'
2-
import Box from '../Box'
3-
import styled from 'styled-components'
4-
import sx, {merge, SxProp} from '../sx'
5-
import {useTheme, Theme} from '../ThemeProvider'
6-
import {VariantType, ButtonProps} from './types'
2+
import {ButtonProps} from './types'
3+
import ButtonBase from './button-base'
74

8-
const TEXT_ROW_HEIGHT = '20px' // custom value off the scale
9-
10-
const getVariantStyles = (variant: VariantType = 'default', theme?: Theme) => {
11-
const style = {
12-
default: {
13-
color: 'btn.text',
14-
backgroundColor: 'btn.bg',
15-
boxShadow: `${theme?.shadows.btn.shadow}, ${theme?.shadows.btn.insetShadow}`,
16-
'&:hover:not([disabled])': {
17-
backgroundColor: 'btn.hoverBg'
18-
},
19-
// focus must come before :active so that the active box shadow overrides
20-
'&:focus:not([disabled])': {
21-
boxShadow: `${theme?.shadows.btn.focusShadow}`
22-
},
23-
'&:active:not([disabled])': {
24-
backgroundColor: 'btn.selectedBg',
25-
boxShadow: `${theme?.shadows.btn.shadowActive}`
26-
},
27-
'&:disabled': {
28-
color: 'primer.fg.disabled',
29-
backgroundColor: 'btn.disabledBg'
30-
}
31-
},
32-
primary: {
33-
color: 'btn.primary.text',
34-
backgroundColor: 'btn.primary.bg',
35-
borderColor: 'border.subtle',
36-
boxShadow: `${theme?.shadows.btn.primary.shadow}`,
37-
'&:hover:not([disabled])': {
38-
color: 'btn.primary.hoverText',
39-
backgroundColor: 'btn.primary.hoverBg'
40-
},
41-
// focus must come before :active so that the active box shadow overrides
42-
'&:focus:not([disabled])': {
43-
boxShadow: `${theme?.shadows.btn.primary.focusShadow}`
44-
},
45-
'&:active:not([disabled])': {
46-
backgroundColor: 'btn.primary.selectedBg',
47-
boxShadow: `${theme?.shadows.btn.primary.selectedShadow}`
48-
},
49-
'&:disabled': {
50-
color: 'btn.primary.disabledText',
51-
backgroundColor: 'btn.primary.disabledBg'
52-
},
53-
'[data-component="ButtonCounter"]': {
54-
backgroundColor: 'btn.primary.counterBg',
55-
color: 'btn.primary.text'
56-
}
57-
},
58-
danger: {
59-
color: 'btn.danger.text',
60-
backgroundColor: 'btn.bg',
61-
boxShadow: `${theme?.shadows.btn.shadow}`,
62-
'&:hover:not([disabled])': {
63-
color: 'btn.danger.hoverText',
64-
backgroundColor: 'btn.danger.hoverBg',
65-
borderColor: 'btn.danger.hoverBorder',
66-
boxShadow: `${theme?.shadows.btn.danger.hoverShadow}`,
67-
'[data-component="ButtonCounter"]': {
68-
backgroundColor: 'btn.danger.hoverCounterBg',
69-
color: 'btn.danger.hoverText'
70-
}
71-
},
72-
// focus must come before :active so that the active box shadow overrides
73-
'&:focus:not([disabled])': {
74-
borderColor: 'btn.danger.focusBorder',
75-
boxShadow: `${theme?.shadows.btn.danger.focusShadow}`
76-
},
77-
'&:active:not([disabled])': {
78-
color: 'btn.danger.selectedText',
79-
backgroundColor: 'btn.danger.selectedBg',
80-
boxShadow: `${theme?.shadows.btn.danger.selectedShadow}`,
81-
borderColor: 'btn.danger.selectedBorder'
82-
},
83-
'&:disabled': {
84-
color: 'btn.danger.disabledText',
85-
backgroundColor: 'btn.danger.disabledBg',
86-
borderColor: 'btn.danger.disabledBorder',
87-
'[data-component="ButtonCounter"]': {
88-
backgroundColor: 'btn.danger.disabledCounterBg'
89-
}
90-
},
91-
'[data-component="ButtonCounter"]': {
92-
color: 'btn.danger.text',
93-
backgroundColor: 'btn.danger.counterBg'
94-
}
95-
},
96-
invisible: {
97-
color: 'accent.fg',
98-
backgroundColor: 'transparent',
99-
border: '0',
100-
boxShadow: 'none',
101-
'&:hover:not([disabled])': {
102-
backgroundColor: 'btn.hoverBg'
103-
},
104-
// focus must come before :active so that the active box shadow overrides
105-
'&:focus:not([disabled])': {
106-
boxShadow: `${theme?.shadows.btn.focusShadow}`
107-
},
108-
'&:active:not([disabled])': {
109-
backgroundColor: 'btn.selectedBg'
110-
},
111-
'&:disabled': {
112-
color: 'primer.fg.disabled'
113-
}
114-
},
115-
outline: {
116-
color: 'btn.outline.text',
117-
boxShadow: `${theme?.shadows.btn.shadow}`,
118-
119-
'&:hover': {
120-
color: 'btn.outline.hoverText',
121-
backgroundColor: 'btn.outline.hoverBg',
122-
borderColor: 'outline.hoverBorder',
123-
boxShadow: `${theme?.shadows.btn.outline.hoverShadow}`,
124-
'[data-component="ButtonCounter"]': {
125-
backgroundColor: 'btn.outline.hoverCounterBg',
126-
color: 'btn.outline.hoverText'
127-
}
128-
},
129-
// focus must come before :active so that the active box shadow overrides
130-
'&:focus': {
131-
borderColor: 'btn.outline.focusBorder',
132-
boxShadow: `${theme?.shadows.btn.outline.focusShadow}`
133-
},
134-
135-
'&:active:not([disabled])': {
136-
color: 'btn.outline.selectedText',
137-
backgroundColor: 'btn.outline.selectedBg',
138-
boxShadow: `${theme?.shadows.btn.outline.selectedShadow}`,
139-
borderColor: 'btn.outline.selectedBorder'
140-
},
141-
142-
'&:disabled': {
143-
color: 'btn.outline.disabledText',
144-
backgroundColor: 'btn.outline.disabledBg',
145-
borderColor: 'btn.border',
146-
'[data-component="ButtonCounter"]': {
147-
backgroundColor: 'btn.outline.disabledCounterBg'
148-
}
149-
},
150-
'[data-component="ButtonCounter"]': {
151-
backgroundColor: 'btn.outline.counterBg',
152-
color: 'btn.outline.text'
153-
}
154-
}
155-
}
156-
return style[variant]
157-
}
158-
159-
const getSizeStyles = (size = 'medium', variant: VariantType = 'default', iconOnly: boolean) => {
160-
let paddingY, paddingX, fontSize
161-
switch (size) {
162-
case 'small':
163-
paddingY = 3
164-
paddingX = 12
165-
fontSize = 0
166-
break
167-
case 'large':
168-
paddingY = 9
169-
paddingX = 20
170-
fontSize = 2
171-
break
172-
case 'medium':
173-
default:
174-
paddingY = 5
175-
paddingX = 16
176-
fontSize = 1
177-
}
178-
if (iconOnly) {
179-
paddingX = paddingY + 2
180-
}
181-
if (variant === 'invisible') {
182-
paddingY = paddingY + 1
183-
}
184-
return {
185-
paddingY: `${paddingY}px`,
186-
paddingX: `${paddingX}px`,
187-
fontSize,
188-
'[data-component="ButtonCounter"]': {
189-
fontSize
190-
}
191-
}
192-
}
193-
194-
const ButtonBase = styled.button<SxProp>(sx)
195-
196-
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
197-
({children, sx: sxProp = {}, ...props}, forwardedRef): JSX.Element => {
198-
const {
199-
icon: Icon,
200-
leadingIcon: LeadingIcon,
201-
trailingIcon: TrailingIcon,
202-
variant = 'default',
203-
size = 'medium'
204-
} = props
205-
const iconOnly = !!Icon
206-
const {theme} = useTheme()
207-
208-
const styles = {
209-
borderRadius: '2',
210-
border: '1px solid',
211-
borderColor: theme?.colors.btn.border,
212-
display: 'grid',
213-
gridTemplateAreas: '"leadingIcon text trailingIcon"',
214-
fontWeight: 'bold',
215-
lineHeight: TEXT_ROW_HEIGHT,
216-
whiteSpace: 'nowrap',
217-
verticalAlign: 'middle',
218-
cursor: 'pointer',
219-
appearance: 'none',
220-
userSelect: 'none',
221-
textDecoration: 'none',
222-
textAlign: 'center',
223-
'& > :not(:last-child)': {
224-
mr: '2'
225-
},
226-
'&:focus': {
227-
outline: 'none'
228-
},
229-
'&:disabled': {
230-
cursor: 'default'
231-
},
232-
'&:disabled svg': {
233-
opacity: '0.6'
234-
},
235-
'[data-component="leadingIcon"]': {
236-
gridArea: 'leadingIcon'
237-
},
238-
'[data-component="text"]': {
239-
gridArea: 'text'
240-
},
241-
'[data-component="trailingIcon"]': {
242-
gridArea: 'trailingIcon'
243-
}
244-
}
245-
const iconWrapStyles = {
246-
display: 'inline-block'
247-
}
248-
const sxStyles = merge.all([
249-
styles,
250-
getSizeStyles(size, variant, iconOnly),
251-
getVariantStyles(variant, theme),
252-
sxProp as SxProp
253-
])
254-
return (
255-
<ButtonBase sx={sxStyles} ref={forwardedRef} {...props}>
256-
{LeadingIcon && (
257-
<Box as="span" data-component="leadingIcon" sx={iconWrapStyles} aria-hidden={!iconOnly}>
258-
<LeadingIcon />
259-
</Box>
260-
)}
261-
<span data-component="text" hidden={Icon ? true : false}>
262-
{children}
263-
</span>
264-
{Icon && (
265-
<Box data-component="icon-only" as="span" sx={{display: 'inline-block'}} aria-hidden={!iconOnly}>
266-
<Icon />
267-
</Box>
268-
)}
269-
{TrailingIcon && (
270-
<Box as="span" data-component="trailingIcon" sx={{...iconWrapStyles, ml: 2}} aria-hidden={!iconOnly}>
271-
<TrailingIcon />
272-
</Box>
273-
)}
274-
</ButtonBase>
275-
)
276-
}
277-
)
5+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({children, ...props}, forwardedRef): JSX.Element => {
6+
return (
7+
<ButtonBase ref={forwardedRef} {...props} as="button">
8+
{children}
9+
</ButtonBase>
10+
)
11+
})
27812

27913
Button.displayName = 'Button'
28014

281-
Object.assign(Button, {})
282-
28315
export {Button}

src/NewButton/icon-button.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, {forwardRef} from 'react'
2+
import {merge, SxProp} from '../sx'
3+
import {useTheme} from '../ThemeProvider'
4+
import Box from '../Box'
5+
import {IconButtonProps, StyledButton} from './types'
6+
import {getBaseStyles, getSizeStyles, getVariantStyles} from './styles'
7+
import {useSSRSafeId} from '@react-aria/ssr'
8+
9+
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, forwardedRef): JSX.Element => {
10+
const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, iconLabel} = props
11+
const iconLabelId = useSSRSafeId()
12+
const {theme} = useTheme()
13+
const styles = {
14+
...getBaseStyles(theme)
15+
}
16+
const sxStyles = merge.all([
17+
styles,
18+
getSizeStyles(size, variant, true),
19+
getVariantStyles(variant, theme),
20+
sxProp as SxProp
21+
])
22+
return (
23+
<StyledButton aria-labelledby={iconLabelId} sx={sxStyles} ref={forwardedRef} {...props}>
24+
<span id={iconLabelId} hidden={true}>
25+
{iconLabel}
26+
</span>
27+
<Box as="span" sx={{display: 'inline-block'}}>
28+
<Icon />
29+
</Box>
30+
</StyledButton>
31+
)
32+
})
33+
34+
export default IconButton

0 commit comments

Comments
 (0)