Skip to content

Commit f9aba22

Browse files
authored
Merge pull request #287 from forestream/feat-input
Feat input
2 parents caf79e8 + 98ab91a commit f9aba22

File tree

11 files changed

+729
-2
lines changed

11 files changed

+729
-2
lines changed

.changeset/breezy-swans-brush.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 Input component

.changeset/config.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"ignore": [
1111
"*-example",
1212
"*-benchmark",
13-
"landing",
14-
"@devup-ui/components"
13+
"landing"
1514
]
1615
}

.changeset/stupid-brooms-see.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+
Fix Input comp test

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ describe('export', () => {
33
const index = await import('../index')
44
expect({ ...index }).toEqual({
55
Button: expect.any(Function),
6+
Input: expect.any(Function),
67
})
78
})
89
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useState } from 'react'
2+
3+
import { Input } from '.'
4+
5+
export function Controlled() {
6+
const [value, setValue] = useState('')
7+
8+
return <Input onChange={(e) => setValue(e.target.value)} value={value} />
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ComponentProps } from 'react'
2+
3+
export function GlassIcon(props: ComponentProps<'svg'>) {
4+
return (
5+
<svg
6+
fill="none"
7+
height="24"
8+
viewBox="0 0 24 24"
9+
width="24"
10+
xmlns="http://www.w3.org/2000/svg"
11+
{...props}
12+
>
13+
<path
14+
clipRule="evenodd"
15+
d="M14.5006 15.9949C13.445 16.6754 12.1959 17.069 10.8571 17.069C7.07005 17.069 4 13.9195 4 10.0345C4 6.14945 7.07005 3 10.8571 3C14.6442 3 17.7143 6.14945 17.7143 10.0345C17.7143 11.7044 17.1471 13.2384 16.1995 14.4448C16.2121 14.4567 16.2245 14.4688 16.2367 14.4813L19.6653 17.9986C20.1116 18.4564 20.1116 19.1988 19.6653 19.6566C19.2189 20.1145 18.4953 20.1145 18.049 19.6566L14.6204 16.1394C14.5761 16.0938 14.5361 16.0455 14.5006 15.9949ZM16.2143 10.0345C16.2143 13.1274 13.7799 15.569 10.8571 15.569C7.93435 15.569 5.5 13.1274 5.5 10.0345C5.5 6.94154 7.93435 4.5 10.8571 4.5C13.7799 4.5 16.2143 6.94154 16.2143 10.0345Z"
16+
fill="currentColor"
17+
fillRule="evenodd"
18+
/>
19+
</svg>
20+
)
21+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Meta, StoryObj } from '@storybook/react-vite'
2+
3+
import { Controlled } from './Controlled'
4+
import { GlassIcon } from './GlassIcon'
5+
import { Input } from './index'
6+
7+
type Story = StoryObj<typeof meta>
8+
9+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
10+
const meta: Meta<typeof Input> = {
11+
title: 'Devfive/Input',
12+
component: Input,
13+
decorators: [
14+
(Story) => (
15+
<div style={{ padding: '10px' }}>
16+
<Story />
17+
</div>
18+
),
19+
],
20+
}
21+
22+
export const Default: Story = {
23+
args: {
24+
placeholder: 'Input text',
25+
},
26+
}
27+
28+
export const ControlledInput: Story = {
29+
args: {
30+
placeholder: 'Input text',
31+
},
32+
render: () => <Controlled />,
33+
}
34+
35+
export const Error: Story = {
36+
args: {
37+
placeholder: 'Input text',
38+
error: true,
39+
errorMessage: 'Error message',
40+
},
41+
}
42+
43+
export const Disabled: Story = {
44+
args: {
45+
placeholder: 'Input text',
46+
disabled: true,
47+
},
48+
}
49+
50+
export const WithIcon: Story = {
51+
args: {
52+
placeholder: 'Input text',
53+
allowClear: true,
54+
icon: <GlassIcon />,
55+
},
56+
}
57+
58+
export default meta

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

Lines changed: 247 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { fireEvent, render } from '@testing-library/react'
2+
import { DevupThemeTypography } from 'node_modules/@devup-ui/react/dist/types/typography'
3+
4+
import { ClearButton, Input } from '..'
5+
import { Controlled } from '../Controlled'
6+
import { GlassIcon } from '../GlassIcon'
7+
8+
describe('Input', () => {
9+
it('should render with default props', () => {
10+
const { container } = render(<Input />)
11+
expect(container).toMatchSnapshot()
12+
})
13+
14+
it('should render with disabled prop', () => {
15+
const { container } = render(<Input disabled />)
16+
expect(container).toMatchSnapshot()
17+
})
18+
19+
it('should render with allowClear prop', () => {
20+
const { container } = render(<Input allowClear />)
21+
expect(container).toMatchSnapshot()
22+
expect(container.querySelector('[aria-label="input"]')).toHaveClass(
23+
'padding-right-0-36px--1',
24+
)
25+
})
26+
27+
it('should not have padding right when allowClear is false', () => {
28+
const { container } = render(<Input allowClear={false} />)
29+
expect(container).toMatchSnapshot()
30+
expect(container.querySelector('[aria-label="input"]')).not.toHaveClass(
31+
'padding-right-0-36px--1',
32+
)
33+
})
34+
35+
it('should show clear button when value is not empty', () => {
36+
const { container } = render(<Input />)
37+
expect(container).toMatchSnapshot()
38+
fireEvent.change(container.querySelector('input')!, {
39+
target: { value: 'test' },
40+
})
41+
expect(container.querySelector('button')).toBeInTheDocument()
42+
})
43+
44+
it('should not show clear button when value is empty', () => {
45+
const { container } = render(<Input />)
46+
expect(container).toMatchSnapshot()
47+
})
48+
49+
it('should be able to clear value by clicking clear button', () => {
50+
const { container } = render(<Input allowClear />)
51+
fireEvent.change(container.querySelector('input')!, {
52+
target: { value: 'test' },
53+
})
54+
expect(container.querySelector('button')).toBeInTheDocument()
55+
fireEvent.click(container.querySelector('button')!)
56+
expect(container.querySelector('input')!.value).toBe('')
57+
})
58+
59+
it('should be able to render with icon', () => {
60+
const { container } = render(
61+
<Input icon={<GlassIcon data-testid="icon" />} />,
62+
)
63+
expect(container.querySelector('[data-testid="icon"]')).toBeInTheDocument()
64+
})
65+
66+
it('should render error style when error is true', () => {
67+
const { container } = render(<Input error />)
68+
expect(container).toMatchSnapshot()
69+
expect(container.querySelector('[aria-label="input"]')).toHaveClass(
70+
'border-color-0-var(--error,light-dark(#D52B2E,#FF5B5E))--1',
71+
)
72+
})
73+
74+
it('should be able to render with error message', () => {
75+
const { container } = render(<Input errorMessage="Error message" />)
76+
expect(
77+
container.querySelector('[aria-label="error-message"]'),
78+
).toBeInTheDocument()
79+
})
80+
81+
it('should pass colors prop', () => {
82+
const { container } = render(
83+
<Input
84+
colors={{
85+
primary: 'red',
86+
error: 'blue',
87+
text: 'green',
88+
}}
89+
/>,
90+
)
91+
const input = container.querySelector('[aria-label="input"]')
92+
expect(input).toHaveStyle({
93+
'--primary': 'red',
94+
'--error': 'blue',
95+
'--text': 'green',
96+
})
97+
})
98+
99+
it('should have typography when typography is provided', () => {
100+
const { container } = render(
101+
<Input typography={'inlineLabelS' as keyof DevupThemeTypography} />,
102+
)
103+
expect(container).toMatchSnapshot()
104+
expect(container.querySelector('input')).toHaveClass('typo-inlineLabelS')
105+
})
106+
107+
it('should pass className prop to error message component', () => {
108+
const { container } = render(
109+
<Input
110+
classNames={{
111+
errorMessage: 'error-message',
112+
}}
113+
errorMessage="Error message"
114+
/>,
115+
)
116+
expect(container).toMatchSnapshot()
117+
expect(container.querySelector('[aria-label="error-message"]')).toHaveClass(
118+
'error-message',
119+
)
120+
})
121+
122+
it('should pass className prop to icon component', () => {
123+
const { container } = render(
124+
<Input
125+
classNames={{
126+
icon: 'icon',
127+
}}
128+
icon={<GlassIcon />}
129+
/>,
130+
)
131+
expect(container).toMatchSnapshot()
132+
expect(container.querySelector('[aria-label="icon"]')).toHaveClass('icon')
133+
})
134+
135+
it('should pass props to ClearButton component', async () => {
136+
const { container } = render(<ClearButton />)
137+
expect(container).toMatchSnapshot()
138+
const clearButton = container.querySelector('[aria-label="clear-button"]')
139+
expect(clearButton).toBeInTheDocument()
140+
})
141+
142+
it('should render disabled icon style when disabled is true', () => {
143+
const { container } = render(<Input disabled icon={<GlassIcon />} />)
144+
expect(container).toMatchSnapshot()
145+
expect(container.querySelector('[aria-label="icon"]')).toHaveClass(
146+
'color-0-var(--inputDisabledText,light-dark(#D6D7DE,#373737))--1',
147+
)
148+
})
149+
150+
it('should call onChange prop when it is provided andvalue is changed', () => {
151+
const onChange = vi.fn()
152+
const { container } = render(<Input onChange={onChange} />)
153+
fireEvent.change(container.querySelector('input')!, {
154+
target: { value: 'test' },
155+
})
156+
expect(onChange).toHaveBeenCalledWith(expect.any(Object))
157+
})
158+
})
159+
160+
describe('Controlled Input', () => {
161+
it('should render with value', () => {
162+
const { container } = render(<Controlled />)
163+
expect(container).toMatchSnapshot()
164+
})
165+
166+
it('should update value when it is changed', () => {
167+
const { container } = render(<Controlled />)
168+
fireEvent.change(container.querySelector('input')!, {
169+
target: { value: 'test' },
170+
})
171+
expect(container.querySelector('input')!.value).toBe('test')
172+
})
173+
})

0 commit comments

Comments
 (0)