Skip to content

Commit bd6f963

Browse files
authored
feat: strip honorable out of Button component (#699)
1 parent e295c8d commit bd6f963

File tree

17 files changed

+450
-338
lines changed

17 files changed

+450
-338
lines changed

src/components/Button.tsx

Lines changed: 323 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,325 @@
1-
import { Button as HonorableButton } from 'honorable'
2-
import type { ButtonProps as HonorableButtonProps } from 'honorable'
3-
import { keyframes } from '@emotion/react'
4-
5-
export type ButtonProps = HonorableButtonProps & { pulse?: boolean }
6-
7-
const pulseKeyframes = keyframes`
8-
0% { box-shadow: 0 0 7px 2px #fff1; }
9-
70% { box-shadow: 0 0 7px 4px #fff2; }
10-
100% { box-shadow: 0 0 7px 2px #fff1; }
11-
`
12-
13-
function Button({ pulse = false, ...props }: ButtonProps) {
14-
return (
15-
<HonorableButton
16-
animationIterationCount="infinite"
17-
animationDuration="4s"
18-
animationName={pulse ? pulseKeyframes : undefined}
19-
boxShadow={pulse ? '0 0 7px 2px #fff1' : undefined}
20-
_hover={{ animationPlayState: 'paused' }}
21-
type="button"
22-
{...props}
23-
/>
24-
)
25-
}
1+
import {
2+
ComponentPropsWithRef,
3+
ElementType,
4+
memo,
5+
ReactNode,
6+
useEffect,
7+
useRef,
8+
useState,
9+
} from 'react'
10+
11+
import { styled, useTheme } from 'styled-components'
12+
import { resolveSpacersAndSanitizeCss, SpacerProps } from '../theme/spacing'
13+
import { applyNodeToRefs } from '../utils/applyNodeToRefs'
14+
import Flex from './Flex'
15+
import { Spinner } from './Spinner'
16+
17+
type ButtonSize = 'small' | 'medium' | 'large'
18+
type ButtonType =
19+
| 'primary'
20+
| 'secondary'
21+
| 'tertiary'
22+
| 'tertiaryNoPadding'
23+
| 'floating'
24+
| 'destructive'
25+
26+
export type ButtonProps = {
27+
startIcon?: ReactNode
28+
endIcon?: ReactNode
29+
loading?: boolean
30+
loadingIndicator?: ReactNode
31+
children?: ReactNode
32+
// flags- keeping this pattern instead of using "size" and "type" for backwards compatibility
33+
small?: boolean
34+
large?: boolean
35+
secondary?: boolean
36+
tertiary?: boolean
37+
floating?: boolean
38+
destructive?: boolean
39+
// flexible typing for links
40+
as?: ElementType
41+
to?: string
42+
href?: string
43+
target?: string | '_blank' | '_self' | '_parent' | '_top'
44+
rel?: string | 'noopener noreferrer' | 'noreferrer' | 'noopener'
45+
} & SpacerProps &
46+
ComponentPropsWithRef<'button'>
47+
48+
const Button = memo(
49+
({
50+
ref,
51+
startIcon,
52+
endIcon,
53+
loading,
54+
loadingIndicator,
55+
disabled,
56+
children,
57+
small,
58+
large,
59+
secondary,
60+
tertiary,
61+
destructive,
62+
floating,
63+
...props
64+
}: ButtonProps) => {
65+
const theme = useTheme()
66+
const buttonRef = useRef<HTMLButtonElement>(null)
67+
const [height, setHeight] = useState<number | 'auto'>('auto')
68+
69+
const buttonSize = large ? 'large' : small ? 'small' : 'medium'
70+
const buttonType = secondary
71+
? 'secondary'
72+
: tertiary
73+
? 'tertiary'
74+
: destructive
75+
? 'destructive'
76+
: floating
77+
? 'floating'
78+
: 'primary'
79+
80+
useEffect(() => {
81+
if (!buttonRef.current) return
82+
83+
setHeight(buttonRef.current.offsetHeight)
84+
}, [])
85+
86+
const { rest, css } = resolveSpacersAndSanitizeCss(props, theme)
87+
88+
return (
89+
<ButtonBaseSC
90+
ref={(node: HTMLButtonElement) =>
91+
applyNodeToRefs([buttonRef, ref], node)
92+
}
93+
$size={buttonSize}
94+
$type={buttonType}
95+
$noPadding={props.padding === 'none'}
96+
disabled={disabled}
97+
css={css}
98+
{...(loading && { inert: true })}
99+
{...rest}
100+
>
101+
{!!startIcon && (
102+
<IconSC
103+
$position="start"
104+
$size={buttonSize}
105+
$loading={loading}
106+
>
107+
{startIcon}
108+
</IconSC>
109+
)}
110+
{loading && (
111+
<LoadingIndicatorWrapperSC>
112+
{loadingIndicator || (
113+
<Spinner
114+
color={theme.colors.text}
115+
size={typeof height === 'number' ? (height * 3) / 5 : 16}
116+
/>
117+
)}
118+
</LoadingIndicatorWrapperSC>
119+
)}
120+
<Flex
121+
align="center"
122+
justify="center"
123+
visibility={loading ? 'hidden' : 'inherit'}
124+
>
125+
{children}
126+
</Flex>
127+
{!!endIcon && (
128+
<IconSC
129+
$position="end"
130+
$size={buttonSize}
131+
$loading={loading}
132+
>
133+
{endIcon}
134+
</IconSC>
135+
)}
136+
</ButtonBaseSC>
137+
)
138+
}
139+
)
140+
141+
const ButtonBaseSC = styled.button<{
142+
$size: ButtonSize
143+
$type: ButtonType
144+
$noPadding: boolean
145+
}>(
146+
({
147+
theme: { colors, spacing, partials, borderRadiuses, boxShadows },
148+
$size,
149+
$type,
150+
$noPadding,
151+
}) => ({
152+
// default styles that were baked into honorable (and not already being overridden)
153+
cursor: 'pointer',
154+
position: 'relative',
155+
display: 'flex',
156+
alignItems: 'center',
157+
justifyContent: 'center',
158+
alignContent: 'center',
159+
minHeight: 38,
160+
userSelect: 'none',
161+
textDecoration: 'none',
162+
transition:
163+
'color 150ms ease, background-color 150ms ease, border 150ms ease',
164+
flexShrink: 0,
165+
// primary/baseline styles
166+
...partials.text.buttonMedium,
167+
borderRadius: borderRadiuses.medium,
168+
color: colors['text-always-white'],
169+
background: colors['action-primary'],
170+
border: '1px solid transparent',
171+
padding: `${spacing.xsmall - 1}px ${spacing.medium - 1}px`,
172+
'&:focus': { outline: 'none' },
173+
'&:focus-visible': {
174+
outline: 'none',
175+
borderColor: colors['border-outline-focused'],
176+
},
177+
'&:hover': { background: colors['action-primary-hover'] },
178+
'&:active': { background: colors['action-primary'] },
179+
'&:disabled': {
180+
cursor: 'not-allowed',
181+
color: colors['text-primary-disabled'],
182+
'&:hover': { background: colors['action-primary-disabled'] },
183+
background: colors['action-primary-disabled'],
184+
},
185+
// secondary styles
186+
...($type === 'secondary' && {
187+
color: colors['text-light'],
188+
background: 'transparent',
189+
borderColor: colors['border-input'],
190+
'&:hover': {
191+
color: colors['text'],
192+
background: colors['action-input-hover'],
193+
borderColor: colors['border-input'],
194+
},
195+
'&:active': { color: colors['text'], background: 'transparent' },
196+
'&:focus-visible': {
197+
color: colors['text'],
198+
background: colors['action-input-hover'],
199+
},
200+
'&:disabled': {
201+
cursor: 'not-allowed',
202+
color: colors['text-disabled'],
203+
background: 'transparent',
204+
},
205+
}),
206+
// tertiary styles
207+
...($type === 'tertiary' && {
208+
color: colors['text-light'],
209+
background: 'transparent',
210+
borderColor: 'transparent',
211+
'&:hover': {
212+
color: colors['text'],
213+
background: colors['action-input-hover'],
214+
},
215+
'&:active': { background: 'transparent', color: colors['text'] },
216+
'&:focus-visible': {
217+
color: colors['text'],
218+
background: colors['action-input-hover'],
219+
},
220+
'&:disabled': {
221+
cursor: 'not-allowed',
222+
color: colors['text-disabled'],
223+
background: 'transparent',
224+
},
225+
// tertiary no padding styles
226+
...($noPadding && {
227+
paddingLeft: 0,
228+
paddingRight: 0,
229+
'&:active': { color: colors['text-light'] },
230+
'&:hover, &:active, &:focus-visible': {
231+
background: 'transparent',
232+
textDecoration: 'underline',
233+
},
234+
}),
235+
}),
236+
// destructive styles
237+
...($type === 'destructive' && {
238+
color: colors['text-danger'],
239+
background: 'transparent',
240+
borderColor: colors['border-danger'],
241+
'&:hover': { background: colors['action-input-hover'] },
242+
'&:focus-visible': { background: colors['action-input-hover'] },
243+
'&:active': { background: 'transparent' },
244+
'&:disabled': {
245+
cursor: 'not-allowed',
246+
color: colors['text-disabled'],
247+
borderColor: colors['border-disabled'],
248+
'&:hover': { background: 'transparent' },
249+
},
250+
}),
251+
// floating styles
252+
...($type === 'floating' && {
253+
color: colors['text-light'],
254+
background: colors['fill-two'],
255+
borderColor: colors['border-input'],
256+
boxShadow: boxShadows.slight,
257+
'&:hover': {
258+
color: colors['text'],
259+
background: colors['fill-two'],
260+
borderColor: colors['border-input'],
261+
boxShadow: boxShadows.moderate,
262+
},
263+
'&:active': {
264+
color: colors['text'],
265+
background: colors['fill-two-hover'],
266+
borderColor: colors['border-input'],
267+
},
268+
'&:focus-visible': {
269+
color: colors['text'],
270+
background: colors['fill-two-selected'],
271+
},
272+
'&:disabled': {
273+
cursor: 'not-allowed',
274+
color: colors['text-disabled'],
275+
borderColor: colors['border-input'],
276+
background: 'transparent',
277+
'&:hover': {
278+
borderColor: colors['border-input'],
279+
background: 'transparent',
280+
},
281+
},
282+
}),
283+
// sizes besides medium (default)
284+
...($size === 'large' && {
285+
...partials.text.buttonLarge,
286+
padding: `${spacing.small - 1}px ${spacing.large - 1}px`,
287+
}),
288+
...($size === 'small' && {
289+
...partials.text.buttonSmall,
290+
padding: `${spacing.xxsmall - 1}px ${spacing.medium - 1}px`,
291+
minHeight: 32,
292+
}),
293+
})
294+
)
295+
296+
const IconSC = styled.span<{
297+
$loading: boolean
298+
$size: ButtonSize
299+
$position: 'start' | 'end'
300+
}>(({ theme, $loading, $size, $position }) => {
301+
const marginSize =
302+
$size === 'large' ? theme.spacing.medium : theme.spacing.small
303+
return {
304+
display: 'flex',
305+
alignItems: 'center',
306+
justifyContent: 'center',
307+
visibility: $loading ? 'hidden' : 'inherit',
308+
// adapted from honorable theme styles
309+
margin:
310+
$position === 'start' ? `0 ${marginSize}px 0 0` : `0 0 0 ${marginSize}px`,
311+
}
312+
})
313+
314+
const LoadingIndicatorWrapperSC = styled.span({
315+
display: 'flex',
316+
alignItems: 'center',
317+
justifyContent: 'center',
318+
position: 'absolute',
319+
left: 0,
320+
right: 0,
321+
top: 0,
322+
bottom: 0,
323+
})
26324

27325
export default Button

src/components/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type ComponentPropsWithRef, type ReactNode, useCallback } from 'react'
44

55
import styled, { useTheme } from 'styled-components'
66

7-
import { type Nullable, type SeverityExt } from '../types'
7+
import { type SeverityExt } from '../types'
88

99
import Card from './Card'
1010
import CheckRoundedIcon from './icons/CheckRoundedIcon'

src/components/TabList.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import { useTab, useTabList } from 'react-aria'
2020
import { type TabListState, useTabListState } from 'react-stately'
2121
import styled, { useTheme } from 'styled-components'
2222

23-
import { type Nullable } from '../types'
24-
2523
import ArrowScroll from './ArrowScroll'
2624
import { useItemWrappedChildren } from './ListBox'
2725
import WrapWithIf from './WrapWithIf'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import createIcon from './createIcon'
2+
3+
export default createIcon(({ size, color }) => (
4+
<svg
5+
width={size}
6+
height={size}
7+
viewBox="0 0 16 16"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
>
11+
<path
12+
d="M13.7297 2.34296C12.9205 1.4936 11.8468 1.01064 10.7108 1.01064C9.73043 1.01064 8.76562 1.36037 8.0031 2.02653C6.15129 0.427747 3.44359 0.727519 1.94968 2.70935C0.844815 4.1749 0.6892 6.23999 1.56064 7.87209H3.64588L5.80893 3.85847L7.22503 8.45498L8.8123 5.4406L10.1506 7.88874H11.4266C11.6912 7.88874 11.8935 8.10524 11.8935 8.38836C11.8935 8.67148 11.6912 8.88798 11.4266 8.88798H9.6215L8.82786 7.45574L6.99161 10.9031L5.55995 6.28996L4.17498 8.87133H2.27647L8.0031 15L13.7609 8.83802C15.4259 7.03939 15.4104 4.12494 13.7297 2.34296Z"
13+
fill={color}
14+
/>
15+
</svg>
16+
))

src/components/table/Table.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,9 @@ function Table({
456456
{hover && scrollTop > scrollTopMargin && (
457457
<Button
458458
small
459-
position="absolute"
460-
right="24px"
461-
bottom="24px"
462-
width="140px"
463459
floating
460+
width={140}
461+
css={{ position: 'absolute', right: 24, bottom: 24 }}
464462
endIcon={<CaretUpIcon />}
465463
onClick={() =>
466464
tableContainerRef?.current?.scrollTo({

0 commit comments

Comments
 (0)