11import type React from "react"
22import {
3- type CSSProperties ,
43 forwardRef ,
54 type HTMLProps ,
65 useMemo ,
76 useRef ,
7+ type CSSProperties ,
88} from "react"
9- import { twJoin , twMerge } from "tailwind-merge"
9+ import { twMerge } from "tailwind-merge"
10+ import { cva , cx , type VariantProps } from "class-variance-authority"
1011import { LoadingSpinner } from "./LoadingSpinner"
1112
1213export type ButtonAppearance =
@@ -20,93 +21,211 @@ export type ButtonAppearance =
2021 | "success"
2122 | "information"
2223
24+ type ButtonVariantProps = VariantProps < typeof buttonVariants >
25+ const buttonVariants = cva (
26+ "focus-visible:outline-selected-bold relative box-border flex shrink-0 cursor-pointer items-center justify-center gap-1 rounded-sm border-2 border-transparent px-3 py-1 outline-none outline-2 outline-offset-4 focus-visible:outline-solid" ,
27+ {
28+ variants : {
29+ appearance : {
30+ // those entries are undefined, they just establish the variant for the compoundVariants
31+ default : undefined ,
32+ primary : undefined ,
33+
34+ subtle : undefined ,
35+ link : undefined ,
36+ "subtle-link" : undefined ,
37+ warning : undefined ,
38+
39+ danger : undefined ,
40+
41+ success : undefined ,
42+
43+ information : undefined ,
44+ } ,
45+ disabled : {
46+ true : "disabled:bg-disabled disabled:text-disabled-text disabled:cursor-not-allowed" ,
47+ } ,
48+ selected : {
49+ true : "bg-selected active:bg-selected hover:bg-selected text-selected-text-inverse cursor-pointer" ,
50+ } ,
51+ inverted : {
52+ true : undefined ,
53+ } ,
54+ loading : {
55+ true : "cursor-progress" ,
56+ } ,
57+ } ,
58+ compoundVariants : [
59+ {
60+ inverted : true ,
61+ disabled : true ,
62+ className : "disabled:border-border disabled:bg-transparent" ,
63+ } ,
64+ {
65+ inverted : false ,
66+ appearance : "default" ,
67+ disabled : false ,
68+ className :
69+ "bg-neutral hover:bg-neutral-hovered active:bg-neutral-pressed text-text" ,
70+ } ,
71+ {
72+ inverted : false ,
73+ appearance : "primary" ,
74+ disabled : false ,
75+ className :
76+ "bg-brand-bold hover:bg-brand-bold-hovered active:bg-brand-bold-pressed text-text-inverse" ,
77+ } ,
78+ {
79+ inverted : false ,
80+ appearance : "subtle" ,
81+ disabled : false ,
82+ className :
83+ "bg-neutral-subtle hover:bg-neutral-subtle-hovered active:bg-neutral-subtle-pressed text-text" ,
84+ } ,
85+ {
86+ inverted : false ,
87+ appearance : "link" ,
88+ disabled : false ,
89+ className :
90+ "bg-transparent disabled:bg-transparent text-link hover:underline" ,
91+ } ,
92+ {
93+ inverted : false ,
94+ appearance : "subtle-link" ,
95+ disabled : false ,
96+ className :
97+ "bg-transparent text-text-subtlest hover:text-text-subtle hover:underline" ,
98+ } ,
99+ {
100+ inverted : false ,
101+ appearance : "warning" ,
102+ disabled : false ,
103+ className :
104+ "bg-warning-bold hover:bg-warning-bold-hovered active:bg-warning-bold-pressed text-text-inverse" ,
105+ } ,
106+ {
107+ inverted : false ,
108+ appearance : "danger" ,
109+ disabled : false ,
110+ className :
111+ "bg-danger-bold hover:bg-danger-bold-hovered active:bg-danger-bold-pressed text-text-inverse" ,
112+ } ,
113+ {
114+ inverted : false ,
115+ appearance : "success" ,
116+ disabled : false ,
117+ className :
118+ "bg-success-bold hover:bg-success-bold-hovered active:bg-success-bold-pressed text-text-inverse" ,
119+ } ,
120+ {
121+ inverted : false ,
122+ appearance : "information" ,
123+ disabled : false ,
124+ className :
125+ "bg-information-bold hover:bg-information-bold-hovered active:bg-information-bold-pressed text-text-inverse" ,
126+ } ,
127+ {
128+ inverted : true ,
129+ appearance : "default" ,
130+ className :
131+ "bg-transparent border-neutral-bold border-solid hover:bg-neutral-hovered active:bg-neutral-pressed" ,
132+ } ,
133+ {
134+ inverted : true ,
135+ appearance : "primary" ,
136+ className : cx (
137+ "bg-brand hover:bg-brand-hovered active:bg-brand-pressed" ,
138+ "border-brand-bold text-brand-text border-solid" ,
139+ ) ,
140+ } ,
141+ {
142+ inverted : true ,
143+ appearance : "warning" ,
144+ className : cx (
145+ "bg-warning hover:bg-warning-hovered active:bg-warning-pressed" ,
146+ "border-warning-bold text-warning-text border-solid" ,
147+ ) ,
148+ } ,
149+ {
150+ inverted : true ,
151+ appearance : "danger" ,
152+ className : cx (
153+ "bg-danger hover:bg-danger-hovered active:bg-danger-pressed" ,
154+ "border-danger-bold text-danger-text border-solid" ,
155+ ) ,
156+ } ,
157+ {
158+ inverted : true ,
159+ appearance : "success" ,
160+ className : cx (
161+ "bg-success hover:bg-success-hovered active:bg-success-pressed" ,
162+ "border-success-bold text-success-text border-solid" ,
163+ ) ,
164+ } ,
165+ {
166+ inverted : true ,
167+ appearance : "information" ,
168+ className : cx (
169+ "bg-information hover:bg-information-hovered active:bg-information-pressed" ,
170+ "border-information-bold text-information-text border-solid" ,
171+ ) ,
172+ } ,
173+ ] ,
174+ defaultVariants : {
175+ appearance : "default" ,
176+ disabled : false ,
177+ selected : false ,
178+ inverted : false ,
179+ loading : false ,
180+ } ,
181+ } ,
182+ )
183+
23184export type ButtonProps = {
24185 appearance ?: ButtonAppearance
25186 label ?: string
26187 title ?: string
27188 iconBefore ?: React . ReactNode
28189 iconAfter ?: React . ReactNode
29- disabled ?: boolean
30- selected ?: boolean
31190 autoFocus ?: boolean
32191 children ?: React . ReactNode
33192 style ?: CSSProperties
34193 className ?: string
35- inverted ?: boolean
36194 id ?: string
37195 href ?: string
38196 download ?: string | true
39197 target ?: "_blank" | "_self" | "_parent" | "_top"
40198 "aria-label" ?: string
41199 testId ?: string
42- } & Pick <
43- React . ButtonHTMLAttributes < HTMLButtonElement > ,
44- | "type"
45- | "onClick"
46- | "onDoubleClick"
47- | "onMouseDown"
48- | "onMouseUp"
49- | "onMouseEnter"
50- | "onMouseLeave"
51- | "onMouseOver"
52- | "onMouseOut"
53- | "onFocus"
54- | "onBlur"
55- | "onKeyDown"
56- | "onKeyPress"
57- | "onKeyUp"
58- | "onPointerDown"
59- | "onTouchStart"
60- | "onTouchEnd"
61- | "onTouchMove"
62- | "onTouchCancel"
63- | "title"
64- | "aria-label"
65- | "tabIndex"
66- | "aria-disabled"
67- >
68-
69- const ButtonStyles : { [ style in ButtonAppearance ] : string } = {
70- primary : twJoin (
71- "bg-brand-bold hover:bg-brand-bold-hovered active:bg-brand-bold-pressed text-text-inverse" ,
72- "data-inverted:bg-brand data-inverted:hover:bg-brand-hovered data-inverted:active:bg-brand-pressed" ,
73- "data-inverted:border-brand-bold data-inverted:text-brand-text data-inverted:border-solid" ,
74- ) ,
75-
76- default : twJoin (
77- "bg-neutral hover:bg-neutral-hovered active:bg-neutral-pressed text-text" ,
78- "data-inverted:bg-transparent data-inverted:border-neutral-bold data-inverted:border-solid data-inverted:hover:bg-neutral-hovered data-inverted:active:bg-neutral-pressed" ,
79- ) ,
80- subtle : "bg-neutral-subtle hover:bg-neutral-subtle-hovered active:bg-neutral-subtle-pressed text-text" ,
81- link : "bg-transparent text-link hover:underline" ,
82- "subtle-link" :
83- "bg-transparent text-text-subtlest hover:text-text-subtle hover:underline" ,
84- warning : twJoin (
85- "bg-warning-bold hover:bg-warning-bold-hovered active:bg-warning-bold-pressed text-text-inverse" ,
86- "data-inverted:bg-warning data-inverted:hover:bg-warning-hovered data-inverted:active:bg-warning-pressed" ,
87- "data-inverted:border-warning-bold data-inverted:text-warning-text data-inverted:border-solid" ,
88- ) ,
89- danger : twJoin (
90- "bg-danger-bold hover:bg-danger-bold-hovered active:bg-danger-bold-pressed text-text-inverse" ,
91- "data-inverted:bg-danger data-inverted:hover:bg-danger-hovered data-inverted:active:bg-danger-pressed" ,
92- "data-inverted:border-danger-bold data-inverted:text-danger-text data-inverted:border-solid" ,
93- ) ,
94- success : twJoin (
95- "bg-success-bold hover:bg-success-bold-hovered active:bg-success-bold-pressed text-text-inverse" ,
96- "data-inverted:bg-success data-inverted:hover:bg-success-hovered data-inverted:active:bg-success-pressed" ,
97- "data-inverted:border-success-bold data-inverted:text-success-text data-inverted:border-solid" ,
98- ) ,
99- information : twJoin (
100- "bg-information-bold hover:bg-information-bold-hovered active:bg-information-bold-pressed text-text-inverse" ,
101- "data-inverted:bg-information data-inverted:hover:bg-information-hovered data-inverted:active:bg-information-pressed" ,
102- "data-inverted:border-information-bold data-inverted:text-information-text data-inverted:border-solid" ,
103- ) ,
104- } as const
105-
106- export const ButtonSelectedStyles =
107- "bg-selected active:bg-selected hover:bg-selected text-selected-text-inverse cursor-pointer" as const
200+ } & ButtonVariantProps &
201+ Pick <
202+ React . ButtonHTMLAttributes < HTMLButtonElement > ,
203+ | "type"
204+ | "onClick"
205+ | "onDoubleClick"
206+ | "onMouseDown"
207+ | "onMouseUp"
208+ | "onMouseEnter"
209+ | "onMouseLeave"
210+ | "onMouseOver"
211+ | "onMouseOut"
212+ | "onFocus"
213+ | "onBlur"
214+ | "onKeyDown"
215+ | "onKeyPress"
216+ | "onKeyUp"
217+ | "onPointerDown"
218+ | "onTouchStart"
219+ | "onTouchEnd"
220+ | "onTouchMove"
221+ | "onTouchCancel"
222+ | "title"
223+ | "aria-label"
224+ | "tabIndex"
225+ | "aria-disabled"
226+ >
108227
109- const Button = forwardRef < HTMLButtonElement , ButtonProps > (
228+ export const Button = forwardRef < HTMLButtonElement , ButtonProps > (
110229 (
111230 {
112231 label = "" ,
@@ -167,17 +286,16 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
167286 data-inverted = { inverted }
168287 id = { id }
169288 className = { twMerge (
170- "focus-visible:outline-selected-bold relative box-border flex shrink-0 cursor-pointer items-center justify-center gap-1 rounded-sm border-2 border-transparent px-3 py-1 outline-none outline-2 outline-offset-4 focus-visible:outline-solid" ,
171- ! disabled ? ButtonStyles [ appearance ] : undefined ,
172- `${
173- appearance !== "subtle" && appearance !== "link"
174- ? "disabled:bg-disabled"
175- : ""
176- } disabled:text-disabled-text data-inverted:disabled:border-border disabled:cursor-not-allowed data-inverted:disabled:bg-transparent`,
177- selected ? ButtonSelectedStyles : undefined ,
289+ buttonVariants ( {
290+ appearance,
291+ disabled,
292+ selected,
293+ inverted,
294+ loading : false ,
295+ } ) ,
178296 className ,
179297 ) }
180- disabled = { disabled }
298+ disabled = { disabled ?? false }
181299 data-testid = { testId }
182300 { ...props }
183301 >
@@ -187,20 +305,28 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
187305 } ,
188306)
189307
190- Button . displayName = "LPButton"
191- export { Button }
192-
193- const loadingSpinnerClassNames : { [ appearance in ButtonAppearance ] : string } = {
194- primary : "border-t-text-inverse border-2" ,
195- default : "border-t-border-bold border-2" ,
196- subtle : "border-t-border-bold border-2" ,
197- link : "border-t-border-bold border-2" ,
198- "subtle-link" : "border-t-border-bold border-2" ,
199- warning : "border-t-text-inverse border-2" ,
200- danger : "border-t-text-inverse border-2" ,
201- success : "border-t-text-inverse border-2" ,
202- information : "border-t-text-inverse border-2" ,
203- }
308+ const loadingSpinnerClassNames = cva ( null , {
309+ variants : {
310+ appearance : {
311+ primary : "border-t-text-inverse border-2" ,
312+ default : "border-t-border-bold border-2" ,
313+ subtle : "border-t-border-bold border-2" ,
314+ link : "border-t-border-bold border-2" ,
315+ "subtle-link" : "border-t-border-bold border-2" ,
316+ warning : "border-t-text-inverse border-2" ,
317+ danger : "border-t-text-inverse border-2" ,
318+ success : "border-t-text-inverse border-2" ,
319+ information : "border-t-text-inverse border-2" ,
320+ } ,
321+ loading : {
322+ false : "opacity-0" ,
323+ } ,
324+ } ,
325+ defaultVariants : {
326+ appearance : "default" ,
327+ loading : false ,
328+ } ,
329+ } )
204330
205331export const LoadingButton = ( {
206332 loading = false ,
@@ -235,7 +361,10 @@ export const LoadingButton = ({
235361 >
236362 < LoadingSpinner
237363 className = { twMerge (
238- loadingSpinnerClassNames [ props . appearance ?? "default" ] ,
364+ loadingSpinnerClassNames ( {
365+ appearance : props . appearance ?? "default" ,
366+ loading,
367+ } ) ,
239368 loadingSpinnerClassName ,
240369 ) }
241370 style = { {
0 commit comments