Skip to content

Commit cf6d17c

Browse files
authored
Merge pull request #421 from forestream/feat-LFOP-40
Feat lfop 40
2 parents bafaf63 + 8aae941 commit cf6d17c

File tree

6 files changed

+307
-4
lines changed

6 files changed

+307
-4
lines changed

.changeset/vast-plums-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@devup-ui/components': patch
3+
---
4+
5+
add loading prop in Button in @devup-ui/components

packages/components/src/components/Button/Button.stories.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,11 @@ export const WithForm: Story = {
108108
],
109109
}
110110

111+
export const WithLoading: Story = {
112+
args: {
113+
children: 'Submit',
114+
loading: true,
115+
},
116+
}
117+
111118
export default meta
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Box, keyframes } from '@devup-ui/react'
2+
import { SVGProps } from 'react'
3+
4+
const spin = keyframes({
5+
'0%': {
6+
transform: 'rotateZ(0deg)',
7+
},
8+
'100%': {
9+
transform: 'rotateZ(360deg)',
10+
},
11+
})
12+
13+
interface IconSpinnerProps extends SVGProps<SVGSVGElement> {
14+
type?: 'whole' | 'partial'
15+
}
16+
17+
export function IconSpinner({ type = 'whole', ...props }: IconSpinnerProps) {
18+
if (type === 'partial') {
19+
return (
20+
<Box
21+
animationDuration="1s"
22+
animationIterationCount="infinite"
23+
animationName={spin}
24+
animationTimingFunction="linear"
25+
aria-label="Partial loading spinner"
26+
as="svg"
27+
props={{
28+
fill: 'none',
29+
height: '20',
30+
viewBox: '0 0 20 20',
31+
width: '20',
32+
xmlns: 'http://www.w3.org/2000/svg',
33+
...props,
34+
}}
35+
>
36+
<path
37+
d="M17 10C17 11.291 16.643 12.5568 15.9685 13.6575C15.294 14.7582 14.3282 15.651 13.1779 16.237C12.0277 16.8231 10.7378 17.0797 9.45078 16.9784C8.1638 16.8771 6.9299 16.4219 5.8855 15.6631"
38+
stroke="light-dark(var(--primary, #272727), var(--primary, #F6F6F6))"
39+
strokeLinecap="round"
40+
strokeWidth="3"
41+
/>
42+
</Box>
43+
)
44+
}
45+
return (
46+
<Box
47+
animationDuration="1s"
48+
animationIterationCount="infinite"
49+
animationName={spin}
50+
animationTimingFunction="linear"
51+
aria-label="Whole loading spinner"
52+
as="svg"
53+
props={{
54+
fill: 'none',
55+
height: '20',
56+
viewBox: '0 0 20 20',
57+
width: '20',
58+
xmlns: 'http://www.w3.org/2000/svg',
59+
...props,
60+
}}
61+
>
62+
<g clipPath="url(#paint0_angular_1842_200_clip_path)">
63+
<g transform="matrix(0 0.007 -0.007 0 10 10)">
64+
<foreignObject
65+
height="2857.14"
66+
width="2857.14"
67+
x="-1428.57"
68+
y="-1428.57"
69+
>
70+
<div
71+
style={{
72+
background:
73+
'conic-gradient(from 90deg, light-dark(var(--primary, #272727), var(--primary, #F6F6F6)) 0deg,rgba(0,0,0,0) 360deg)',
74+
height: '100%',
75+
width: '100%',
76+
opacity: 1,
77+
}}
78+
></div>
79+
</foreignObject>
80+
</g>
81+
</g>
82+
<path d="M17 10H15.5C15.5 13.0376 13.0376 15.5 10 15.5V17V18.5C14.6944 18.5 18.5 14.6944 18.5 10H17ZM10 17V15.5C6.96243 15.5 4.5 13.0376 4.5 10H3H1.5C1.5 14.6944 5.30558 18.5 10 18.5V17ZM3 10H4.5C4.5 6.96243 6.96243 4.5 10 4.5V3V1.5C5.30558 1.5 1.5 5.30558 1.5 10H3ZM10 3V4.5C13.0376 4.5 15.5 6.96243 15.5 10H17H18.5C18.5 5.30558 14.6944 1.5 10 1.5V3Z" />
83+
<defs>
84+
<clipPath id="paint0_angular_1842_200_clip_path">
85+
<path d="M17 10H15.5C15.5 13.0376 13.0376 15.5 10 15.5V17V18.5C14.6944 18.5 18.5 14.6944 18.5 10H17ZM10 17V15.5C6.96243 15.5 4.5 13.0376 4.5 10H3H1.5C1.5 14.6944 5.30558 18.5 10 18.5V17ZM3 10H4.5C4.5 6.96243 6.96243 4.5 10 4.5V3V1.5C5.30558 1.5 1.5 5.30558 1.5 10H3ZM10 3V4.5C13.0376 4.5 15.5 6.96243 15.5 10H17H18.5C18.5 5.30558 14.6944 1.5 10 1.5V3Z" />
86+
</clipPath>
87+
</defs>
88+
</Box>
89+
)
90+
}

packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,174 @@ exports[`Button > should render icon when icon is provided 1`] = `
325325
</div>
326326
`;
327327

328+
exports[`Button > should render loading spinner when icon is provided and loading is true 1`] = `
329+
<div>
330+
<button
331+
aria-label="button"
332+
class=" outline-0-2px solid-17005923944751620165-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-.25s--1 color-0-var(--text,#272727)-15425828959012638752-1 background-0-color-mix(in srgb,var(--primary,#8163E1) 20%,#FFF 80%)-15425828959012638752-1 border-0-1px solid var(--primary,#8163E1)-15425828959012638752-1 color-0-#D6D7DE-14172363753176421546-1 background-color-0-#F0F0F3-14172363753176421546-1 cursor-0-not-allowed-14172363753176421546-1 border-color-0-var(--border,#E4E4E4)-14172363753176421546-1 outline-color-0-var(--primaryFocus,#9385D3)-17005923944751620165-1 border-color-0-var(--primary,#8163E1)-8380715471663921674-1 background-0-color-mix(in srgb,var(--primary,#8163E1) 10%,#FFF 90%)-8380715471663921674-1 color-0-var(--text,#F6F6F6)-2922352740838246662-1 background-0-var(--primary,#8163E1)-2922352740838246662-1 color-0-#373737-878116160589243838-1 background-color-0-#47474A-878116160589243838-1 border-color-0-transparent-878116160589243838-1 border-color-0-var(--primary,#8163E1)-6232724021015440856-1 background-0-color-mix(in srgb,var(--primary,#674DC7) 10%,var(--inputBackground,#2E2E2E) 90%)-6232724021015440856-1 outline-color-0-var(--primaryFocus,#927CE4)-13318702800233181468-1 background-0-var(--inputBackground,#2E2E2E)-6667598448774358329-1 background-0-var(--inputBackground,#FFF)--1 border-0-1px solid var(--border,#E4E4E4)--1 border-radius-0-10px--1 color-0-var(--text,#272727)--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--.02em--1 letter-spacing-4--.03em--1 padding-right-0-28px--1 padding-left-0-28px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "
333+
type="button"
334+
>
335+
<div
336+
class="max-width-0-100%--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"
337+
>
338+
<div
339+
class="display-0-flex--0 justify-content-0-center--0 align-items-0-center--0 height-0-24px--255 width-0-24px--255 left-0-4px--255 position-0-absolute--255 color-0-inherit-9970740749223281870-255 top-0-50%--255 transform-0-translate(-100%,-50%)--255"
340+
role="presentation"
341+
>
342+
<svg
343+
aria-label="Whole loading spinner"
344+
class="animation-duration-0-1s--255 animation-iteration-count-0-infinite--255 animation-name-0---255 animation-timing-function-0-linear--255"
345+
fill="none"
346+
height="20"
347+
style="--animation-name-0-: k-420169018174230850;"
348+
viewBox="0 0 20 20"
349+
width="20"
350+
xmlns="http://www.w3.org/2000/svg"
351+
>
352+
<g
353+
clip-path="url(#paint0_angular_1842_200_clip_path)"
354+
>
355+
<g
356+
transform="matrix(0 0.007 -0.007 0 10 10)"
357+
>
358+
<foreignobject
359+
height="2857.14"
360+
width="2857.14"
361+
x="-1428.57"
362+
y="-1428.57"
363+
>
364+
<div
365+
style="height: 100%; width: 100%; opacity: 1;"
366+
/>
367+
</foreignobject>
368+
</g>
369+
</g>
370+
<path
371+
d="M17 10H15.5C15.5 13.0376 13.0376 15.5 10 15.5V17V18.5C14.6944 18.5 18.5 14.6944 18.5 10H17ZM10 17V15.5C6.96243 15.5 4.5 13.0376 4.5 10H3H1.5C1.5 14.6944 5.30558 18.5 10 18.5V17ZM3 10H4.5C4.5 6.96243 6.96243 4.5 10 4.5V3V1.5C5.30558 1.5 1.5 5.30558 1.5 10H3ZM10 3V4.5C13.0376 4.5 15.5 6.96243 15.5 10H17H18.5C18.5 5.30558 14.6944 1.5 10 1.5V3Z"
372+
/>
373+
<defs>
374+
<clippath
375+
id="paint0_angular_1842_200_clip_path"
376+
>
377+
<path
378+
d="M17 10H15.5C15.5 13.0376 13.0376 15.5 10 15.5V17V18.5C14.6944 18.5 18.5 14.6944 18.5 10H17ZM10 17V15.5C6.96243 15.5 4.5 13.0376 4.5 10H3H1.5C1.5 14.6944 5.30558 18.5 10 18.5V17ZM3 10H4.5C4.5 6.96243 6.96243 4.5 10 4.5V3V1.5C5.30558 1.5 1.5 5.30558 1.5 10H3ZM10 3V4.5C13.0376 4.5 15.5 6.96243 15.5 10H17H18.5C18.5 5.30558 14.6944 1.5 10 1.5V3Z"
379+
/>
380+
</clippath>
381+
</defs>
382+
</svg>
383+
</div>
384+
<div
385+
class=" line-height-0-1.2--255 min-height-0-1.2em--255 transform-0-translateX(8px)--255"
386+
/>
387+
</div>
388+
</button>
389+
</div>
390+
`;
391+
392+
exports[`Button > should render loading spinner when loading is true 1`] = `
393+
<div>
394+
<button
395+
aria-label="button"
396+
class=" outline-0-2px solid-17005923944751620165-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-.25s--1 color-0-var(--text,#272727)-15425828959012638752-1 background-0-color-mix(in srgb,var(--primary,#8163E1) 20%,#FFF 80%)-15425828959012638752-1 border-0-1px solid var(--primary,#8163E1)-15425828959012638752-1 color-0-#D6D7DE-14172363753176421546-1 background-color-0-#F0F0F3-14172363753176421546-1 cursor-0-not-allowed-14172363753176421546-1 border-color-0-var(--border,#E4E4E4)-14172363753176421546-1 outline-color-0-var(--primaryFocus,#9385D3)-17005923944751620165-1 border-color-0-var(--primary,#8163E1)-8380715471663921674-1 background-0-color-mix(in srgb,var(--primary,#8163E1) 10%,#FFF 90%)-8380715471663921674-1 color-0-var(--text,#F6F6F6)-2922352740838246662-1 background-0-var(--primary,#8163E1)-2922352740838246662-1 color-0-#373737-878116160589243838-1 background-color-0-#47474A-878116160589243838-1 border-color-0-transparent-878116160589243838-1 border-color-0-var(--primary,#8163E1)-6232724021015440856-1 background-0-color-mix(in srgb,var(--primary,#674DC7) 10%,var(--inputBackground,#2E2E2E) 90%)-6232724021015440856-1 outline-color-0-var(--primaryFocus,#927CE4)-13318702800233181468-1 background-0-var(--inputBackground,#2E2E2E)-6667598448774358329-1 background-0-var(--inputBackground,#FFF)--1 border-0-1px solid var(--border,#E4E4E4)--1 border-radius-0-10px--1 color-0-var(--text,#272727)--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--.02em--1 letter-spacing-4--.03em--1 padding-right-0-28px--1 padding-left-0-28px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "
397+
type="button"
398+
>
399+
<div
400+
class="max-width-0-100%--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"
401+
>
402+
<div
403+
class="display-0-flex--0 justify-content-0-center--0 align-items-0-center--0 height-0-24px--255 width-0-24px--255 left-0-4px--255 position-0-absolute--255 color-0-inherit-9970740749223281870-255 top-0-50%--255 transform-0-translate(-100%,-50%)--255"
404+
role="presentation"
405+
>
406+
<svg
407+
aria-label="Whole loading spinner"
408+
class="animation-duration-0-1s--255 animation-iteration-count-0-infinite--255 animation-name-0---255 animation-timing-function-0-linear--255"
409+
fill="none"
410+
height="20"
411+
style="--animation-name-0-: k-420169018174230850;"
412+
viewBox="0 0 20 20"
413+
width="20"
414+
xmlns="http://www.w3.org/2000/svg"
415+
>
416+
<g
417+
clip-path="url(#paint0_angular_1842_200_clip_path)"
418+
>
419+
<g
420+
transform="matrix(0 0.007 -0.007 0 10 10)"
421+
>
422+
<foreignobject
423+
height="2857.14"
424+
width="2857.14"
425+
x="-1428.57"
426+
y="-1428.57"
427+
>
428+
<div
429+
style="height: 100%; width: 100%; opacity: 1;"
430+
/>
431+
</foreignobject>
432+
</g>
433+
</g>
434+
<path
435+
d="M17 10H15.5C15.5 13.0376 13.0376 15.5 10 15.5V17V18.5C14.6944 18.5 18.5 14.6944 18.5 10H17ZM10 17V15.5C6.96243 15.5 4.5 13.0376 4.5 10H3H1.5C1.5 14.6944 5.30558 18.5 10 18.5V17ZM3 10H4.5C4.5 6.96243 6.96243 4.5 10 4.5V3V1.5C5.30558 1.5 1.5 5.30558 1.5 10H3ZM10 3V4.5C13.0376 4.5 15.5 6.96243 15.5 10H17H18.5C18.5 5.30558 14.6944 1.5 10 1.5V3Z"
436+
/>
437+
<defs>
438+
<clippath
439+
id="paint0_angular_1842_200_clip_path"
440+
>
441+
<path
442+
d="M17 10H15.5C15.5 13.0376 13.0376 15.5 10 15.5V17V18.5C14.6944 18.5 18.5 14.6944 18.5 10H17ZM10 17V15.5C6.96243 15.5 4.5 13.0376 4.5 10H3H1.5C1.5 14.6944 5.30558 18.5 10 18.5V17ZM3 10H4.5C4.5 6.96243 6.96243 4.5 10 4.5V3V1.5C5.30558 1.5 1.5 5.30558 1.5 10H3ZM10 3V4.5C13.0376 4.5 15.5 6.96243 15.5 10H17H18.5C18.5 5.30558 14.6944 1.5 10 1.5V3Z"
443+
/>
444+
</clippath>
445+
</defs>
446+
</svg>
447+
</div>
448+
<div
449+
class=" line-height-0-1.2--255 min-height-0-1.2em--255 transform-0-translateX(8px)--255"
450+
/>
451+
</div>
452+
</button>
453+
</div>
454+
`;
455+
456+
exports[`Button > should render loading spinner when loading is true and loadingSpinner is partial 1`] = `
457+
<div>
458+
<button
459+
aria-label="button"
460+
class=" outline-0-2px solid-17005923944751620165-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-.25s--1 color-0-var(--text,#272727)-15425828959012638752-1 background-0-color-mix(in srgb,var(--primary,#8163E1) 20%,#FFF 80%)-15425828959012638752-1 border-0-1px solid var(--primary,#8163E1)-15425828959012638752-1 color-0-#D6D7DE-14172363753176421546-1 background-color-0-#F0F0F3-14172363753176421546-1 cursor-0-not-allowed-14172363753176421546-1 border-color-0-var(--border,#E4E4E4)-14172363753176421546-1 outline-color-0-var(--primaryFocus,#9385D3)-17005923944751620165-1 border-color-0-var(--primary,#8163E1)-8380715471663921674-1 background-0-color-mix(in srgb,var(--primary,#8163E1) 10%,#FFF 90%)-8380715471663921674-1 color-0-var(--text,#F6F6F6)-2922352740838246662-1 background-0-var(--primary,#8163E1)-2922352740838246662-1 color-0-#373737-878116160589243838-1 background-color-0-#47474A-878116160589243838-1 border-color-0-transparent-878116160589243838-1 border-color-0-var(--primary,#8163E1)-6232724021015440856-1 background-0-color-mix(in srgb,var(--primary,#674DC7) 10%,var(--inputBackground,#2E2E2E) 90%)-6232724021015440856-1 outline-color-0-var(--primaryFocus,#927CE4)-13318702800233181468-1 background-0-var(--inputBackground,#2E2E2E)-6667598448774358329-1 background-0-var(--inputBackground,#FFF)--1 border-0-1px solid var(--border,#E4E4E4)--1 border-radius-0-10px--1 color-0-var(--text,#272727)--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--.02em--1 letter-spacing-4--.03em--1 padding-right-0-28px--1 padding-left-0-28px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "
461+
type="button"
462+
>
463+
<div
464+
class="max-width-0-100%--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"
465+
>
466+
<div
467+
class="display-0-flex--0 justify-content-0-center--0 align-items-0-center--0 height-0-24px--255 width-0-24px--255 left-0-4px--255 position-0-absolute--255 color-0-inherit-9970740749223281870-255 top-0-50%--255 transform-0-translate(-100%,-50%)--255"
468+
role="presentation"
469+
>
470+
<svg
471+
aria-label="Partial loading spinner"
472+
class="animation-duration-0-1s--255 animation-iteration-count-0-infinite--255 animation-name-0---255 animation-timing-function-0-linear--255"
473+
fill="none"
474+
height="20"
475+
style="--animation-name-0-: k-420169018174230850;"
476+
viewBox="0 0 20 20"
477+
width="20"
478+
xmlns="http://www.w3.org/2000/svg"
479+
>
480+
<path
481+
d="M17 10C17 11.291 16.643 12.5568 15.9685 13.6575C15.294 14.7582 14.3282 15.651 13.1779 16.237C12.0277 16.8231 10.7378 17.0797 9.45078 16.9784C8.1638 16.8771 6.9299 16.4219 5.8855 15.6631"
482+
stroke="light-dark(var(--primary, #272727), var(--primary, #F6F6F6))"
483+
stroke-linecap="round"
484+
stroke-width="3"
485+
/>
486+
</svg>
487+
</div>
488+
<div
489+
class=" line-height-0-1.2--255 min-height-0-1.2em--255 transform-0-translateX(8px)--255"
490+
/>
491+
</div>
492+
</button>
493+
</div>
494+
`;
495+
328496
exports[`Button > should render primary background color when danger is true and variant is primary 1`] = `
329497
<div>
330498
<button

packages/components/src/components/Button/__tests__/index.browser.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { css, DevupThemeTypography } from '@devup-ui/react'
22
import { render } from '@testing-library/react'
33

4+
import { IconSpinner } from '../IconSpinner'
45
import { Button } from '../index'
56

67
describe('Button', () => {
@@ -179,4 +180,30 @@ describe('Button', () => {
179180
expect(container).toMatchSnapshot()
180181
expect(container.querySelector('button')).toHaveClass('typo-inlineLabelS')
181182
})
183+
184+
it('should render loading spinner when loading is true', () => {
185+
const { container } = render(<Button loading />)
186+
expect(container).toMatchSnapshot()
187+
expect(
188+
container.querySelector('[aria-label="Whole loading spinner"]'),
189+
).toBeInTheDocument()
190+
})
191+
192+
it('should render loading spinner when loading is true and loadingSpinner is partial', () => {
193+
const { container } = render(<Button loading loadingSpinner="partial" />)
194+
expect(container).toMatchSnapshot()
195+
expect(
196+
container.querySelector('[aria-label="Partial loading spinner"]'),
197+
).toBeInTheDocument()
198+
})
199+
200+
it('should render loading spinner when icon is provided and loading is true', () => {
201+
const { container } = render(
202+
<Button icon={<IconSpinner type="partial" />} loading />,
203+
)
204+
expect(container).toMatchSnapshot()
205+
expect(
206+
container.querySelector('[aria-label="Whole loading spinner"]'),
207+
).toBeInTheDocument()
208+
})
182209
})

packages/components/src/components/Button/index.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
type DevupThemeTypography,
77
} from '@devup-ui/react'
88

9+
import { IconSpinner } from './IconSpinner'
10+
911
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
1012
variant?: 'primary' | 'default'
1113
colors?: {
@@ -21,6 +23,8 @@ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
2123
size?: 'sm' | 'md' | 'lg'
2224
icon?: React.ReactNode
2325
ellipsis?: boolean
26+
loading?: boolean
27+
loadingSpinner?: 'whole' | 'partial'
2428
}
2529

2630
export function Button({
@@ -35,6 +39,8 @@ export function Button({
3539
ellipsis = false,
3640
typography,
3741
disabled,
42+
loading = false,
43+
loadingSpinner = 'whole',
3844
...props
3945
}: ButtonProps): React.ReactElement {
4046
return (
@@ -204,7 +210,7 @@ export function Button({
204210
{
205211
false: { sm: '12px', md: '16px', lg: '20px' }[size],
206212
true: { sm: '24px', md: '28px', lg: '32px' }[size],
207-
}[(!!icon).toString()]
213+
}[(!!(icon || loading)).toString()]
208214
}
209215
py={{ sm: '8px', md: '10px', lg: '12px' }[size]}
210216
styleOrder={1}
@@ -222,7 +228,7 @@ export function Button({
222228
{...props}
223229
>
224230
<Box maxW="100%" mx="auto" pos="relative" w="fit-content">
225-
{icon && (
231+
{(icon || loading) && (
226232
<Center
227233
boxSize="24px"
228234
left="4px"
@@ -236,7 +242,7 @@ export function Button({
236242
top="50%"
237243
transform="translate(-100%, -50%)"
238244
>
239-
{icon}
245+
{loading ? <IconSpinner type={loadingSpinner} /> : icon}
240246
</Center>
241247
)}
242248
<Box
@@ -251,7 +257,7 @@ export function Button({
251257
}
252258
lineHeight="1.2"
253259
minH="1.2em"
254-
transform={!!icon && 'translateX(8px)'}
260+
transform={!!(icon || loading) && 'translateX(8px)'}
255261
>
256262
{children}
257263
</Box>

0 commit comments

Comments
 (0)