Skip to content

Commit 2d109b6

Browse files
authored
Merge pull request #299 from forestream/feat-stepper
Feat stepper
2 parents 60bace1 + 6948495 commit 2d109b6

File tree

9 files changed

+491
-0
lines changed

9 files changed

+491
-0
lines changed

.changeset/free-bats-create.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 Stepper component

packages/components/src/__tests__/index.browser.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ describe('export', () => {
44
expect({ ...index }).toEqual({
55
Button: expect.any(Function),
66
Input: expect.any(Function),
7+
Stepper: expect.any(Function),
78
})
89
})
910
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SVGProps } from 'react'
2+
3+
export function IconMinus({ ...props }: SVGProps<SVGSVGElement>) {
4+
return (
5+
<svg
6+
fill="none"
7+
height="28"
8+
viewBox="0 0 28 28"
9+
width="28"
10+
xmlns="http://www.w3.org/2000/svg"
11+
{...props}
12+
>
13+
<path
14+
clipRule="evenodd"
15+
d="M9 14C9 13.4477 9.3731 13 9.83333 13H18.1667C18.6269 13 19 13.4477 19 14C19 14.5523 18.6269 15 18.1667 15H9.83333C9.3731 15 9 14.5523 9 14Z"
16+
fill="currentColor"
17+
fillRule="evenodd"
18+
/>
19+
</svg>
20+
)
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { SVGProps } from 'react'
2+
3+
export function IconPlus({ ...props }: SVGProps<SVGSVGElement>) {
4+
return (
5+
<svg
6+
fill="none"
7+
height="28"
8+
viewBox="0 0 28 28"
9+
width="28"
10+
xmlns="http://www.w3.org/2000/svg"
11+
{...props}
12+
>
13+
<path
14+
d="M14.8333 9.83333C14.8333 9.3731 14.4602 9 14 9C13.5397 9 13.1666 9.3731 13.1666 9.83333V13.1667H9.8333C9.37307 13.1667 8.99997 13.5398 8.99997 14C8.99997 14.4602 9.37307 14.8333 9.8333 14.8333H13.1666V18.1667C13.1666 18.6269 13.5397 19 14 19C14.4602 19 14.8333 18.6269 14.8333 18.1667V14.8333H18.1666C18.6269 14.8333 19 14.4602 19 14C19 13.5398 18.6269 13.1667 18.1666 13.1667H14.8333V9.83333Z"
15+
fill="currentColor"
16+
/>
17+
</svg>
18+
)
19+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Meta, StoryObj } from '@storybook/react-vite'
2+
3+
import {
4+
Stepper,
5+
StepperContainer,
6+
StepperDecreaseButton,
7+
StepperIncreaseButton,
8+
StepperInput,
9+
} from './index'
10+
11+
type Story = StoryObj<typeof meta>
12+
13+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
14+
const meta: Meta<typeof Stepper> = {
15+
title: 'Devfive/Stepper',
16+
component: Stepper,
17+
decorators: [
18+
(Story) => (
19+
<div style={{ padding: '10px' }}>
20+
<Story />
21+
</div>
22+
),
23+
],
24+
}
25+
26+
export const Default: Story = {
27+
args: {},
28+
render: (args) => (
29+
<Stepper {...args}>
30+
<StepperContainer>
31+
<StepperDecreaseButton />
32+
<StepperInput editable={false} />
33+
<StepperIncreaseButton />
34+
</StepperContainer>
35+
</Stepper>
36+
),
37+
}
38+
39+
export default meta
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Stepper > should render 1`] = `
4+
<div>
5+
<div
6+
class="display-0-flex--0 align-items-0-center--1 gap-0-20px--1 "
7+
>
8+
<button
9+
aria-disabled="true"
10+
aria-label="Decrease button"
11+
class="padding-0-0--255 height-0-28px--255 width-0-28px--255 border-radius-0-4px--255 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-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "
12+
disabled=""
13+
type="button"
14+
>
15+
<div
16+
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"
17+
>
18+
<div
19+
class=" line-height-0-1.2--255 min-height-0-1.2em--255 "
20+
>
21+
<svg
22+
class="color-0-var(--base10,light-dark(#0000001A,#FFFFFF1A))--255"
23+
fill="none"
24+
height="28"
25+
viewBox="0 0 28 28"
26+
width="28"
27+
xmlns="http://www.w3.org/2000/svg"
28+
>
29+
<path
30+
clip-rule="evenodd"
31+
d="M9 14C9 13.4477 9.3731 13 9.83333 13H18.1667C18.6269 13 19 13.4477 19 14C19 14.5523 18.6269 15 18.1667 15H9.83333C9.3731 15 9 14.5523 9 14Z"
32+
fill="currentColor"
33+
fill-rule="evenodd"
34+
/>
35+
</svg>
36+
</div>
37+
</div>
38+
</button>
39+
<div
40+
aria-label="Stepper value"
41+
class="width-0-80px--2 height-0-50px--2 text-align-0-center--2 padding-bottom-0-10px--2 padding-top-0-10px--2 padding-right-0-12px--2 padding-left-0-12px--2 border-radius-0-6px--2 display-0-none-1599573265477977541-2 padding-0-0--3 border-0-none--3 width-0-fit-content--3 height-0-fit-content--3"
42+
data-value="0"
43+
readonly=""
44+
type="number"
45+
value="0"
46+
>
47+
0
48+
</div>
49+
<button
50+
aria-disabled="false"
51+
aria-label="Increase button"
52+
class="padding-0-0--255 height-0-28px--255 width-0-28px--255 border-radius-0-4px--255 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-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "
53+
type="button"
54+
>
55+
<div
56+
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"
57+
>
58+
<div
59+
class=" line-height-0-1.2--255 min-height-0-1.2em--255 "
60+
>
61+
<svg
62+
class="color-0-var(--text,light-dark(#272727,#F6F6F6))--255"
63+
fill="none"
64+
height="28"
65+
viewBox="0 0 28 28"
66+
width="28"
67+
xmlns="http://www.w3.org/2000/svg"
68+
>
69+
<path
70+
d="M14.8333 9.83333C14.8333 9.3731 14.4602 9 14 9C13.5397 9 13.1666 9.3731 13.1666 9.83333V13.1667H9.8333C9.37307 13.1667 8.99997 13.5398 8.99997 14C8.99997 14.4602 9.37307 14.8333 9.8333 14.8333H13.1666V18.1667C13.1666 18.6269 13.5397 19 14 19C14.4602 19 14.8333 18.6269 14.8333 18.1667V14.8333H18.1666C18.6269 14.8333 19 14.4602 19 14C19 13.5398 18.6269 13.1667 18.1666 13.1667H14.8333V9.83333Z"
71+
fill="currentColor"
72+
/>
73+
</svg>
74+
</div>
75+
</div>
76+
</button>
77+
</div>
78+
</div>
79+
`;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { fireEvent, render } from '@testing-library/react'
2+
3+
import {
4+
Stepper,
5+
StepperContainer,
6+
StepperDecreaseButton,
7+
StepperIncreaseButton,
8+
StepperInput,
9+
} from '..'
10+
11+
describe('Stepper', () => {
12+
it('should render', () => {
13+
const { container } = render(
14+
<Stepper>
15+
<StepperContainer>
16+
<StepperDecreaseButton />
17+
<StepperInput editable={false} />
18+
<StepperIncreaseButton />
19+
</StepperContainer>
20+
</Stepper>,
21+
)
22+
expect(container).toMatchSnapshot()
23+
})
24+
25+
it('should throw error if children are used outside of StepperProvider', () => {
26+
expect(() => {
27+
render(<StepperInput editable={false} />)
28+
}).toThrow('useStepper must be used within a StepperProvider')
29+
})
30+
31+
it('should call onValueChange when value is changed', () => {
32+
const onValueChange = vi.fn()
33+
const { container } = render(
34+
<Stepper onValueChange={onValueChange}>
35+
<StepperInput />
36+
</Stepper>,
37+
)
38+
const input = container.querySelector('[aria-label="Stepper value"]')
39+
fireEvent.change(input!, { target: { value: '10' } })
40+
expect(onValueChange).toHaveBeenCalledWith(10)
41+
})
42+
43+
it('should change inner value when onValueChange is not provided', () => {
44+
const { container } = render(
45+
<Stepper>
46+
<StepperInput />
47+
</Stepper>,
48+
)
49+
const input = container.querySelector('[aria-label="Stepper value"]')
50+
fireEvent.change(input!, { target: { value: '10' } })
51+
expect(input).toHaveAttribute('data-value', '10')
52+
})
53+
54+
it('should have disabled decrease button when value is at min', () => {
55+
const { container } = render(
56+
<Stepper>
57+
<StepperDecreaseButton />
58+
<StepperInput />
59+
<StepperIncreaseButton />
60+
</Stepper>,
61+
)
62+
const decreaseButton = container.querySelector(
63+
'[aria-label="Decrease button"] svg',
64+
)
65+
fireEvent.change(container.querySelector('[aria-label="Stepper value"]')!, {
66+
target: { value: '0' },
67+
})
68+
expect(decreaseButton).toHaveClass(
69+
'color-0-var(--base10,light-dark(#0000001A,#FFFFFF1A))--255',
70+
)
71+
})
72+
73+
it('should have disabled increase button when value is at max', () => {
74+
const { container } = render(
75+
<Stepper>
76+
<StepperDecreaseButton />
77+
<StepperInput />
78+
<StepperIncreaseButton />
79+
</Stepper>,
80+
)
81+
const increaseButton = container.querySelector(
82+
'[aria-label="Increase button"] svg',
83+
)
84+
fireEvent.change(container.querySelector('[aria-label="Stepper value"]')!, {
85+
target: { value: '100' },
86+
})
87+
expect(increaseButton).toHaveClass(
88+
'color-0-var(--base10,light-dark(#0000001A,#FFFFFF1A))--255',
89+
)
90+
})
91+
92+
it('should export components', async () => {
93+
const index = await import('../index')
94+
expect({ ...index }).toEqual({
95+
Stepper: expect.any(Function),
96+
StepperContainer: expect.any(Function),
97+
StepperDecreaseButton: expect.any(Function),
98+
StepperIncreaseButton: expect.any(Function),
99+
StepperInput: expect.any(Function),
100+
useStepper: expect.any(Function),
101+
})
102+
})
103+
104+
it('should increase value when increase button is clicked', () => {
105+
const { container } = render(
106+
<Stepper>
107+
<StepperDecreaseButton />
108+
<StepperInput />
109+
<StepperIncreaseButton />
110+
</Stepper>,
111+
)
112+
const increaseButton = container.querySelector(
113+
'[aria-label="Increase button"]',
114+
)
115+
fireEvent.click(increaseButton!)
116+
expect(
117+
container.querySelector('[aria-label="Stepper value"]'),
118+
).toHaveAttribute('data-value', '1')
119+
})
120+
121+
it('should decrease value when decrease button is clicked', () => {
122+
const { container } = render(
123+
<Stepper defaultValue={1}>
124+
<StepperDecreaseButton />
125+
<StepperInput />
126+
<StepperIncreaseButton />
127+
</Stepper>,
128+
)
129+
const decreaseButton = container.querySelector(
130+
'[aria-label="Decrease button"]',
131+
)
132+
fireEvent.click(decreaseButton!)
133+
expect(
134+
container.querySelector('[aria-label="Stepper value"]'),
135+
).toHaveAttribute('data-value', '0')
136+
})
137+
})

0 commit comments

Comments
 (0)