-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: input and label components #70
Changes from 26 commits
7f630c5
ef98ba8
7fe2866
ec3182f
c0fad38
a4c7f9c
cda31f2
a4e9bf2
2381870
061ff75
30cb366
89b89ac
3c0f858
9e59d90
d148975
f263e4b
34461a2
a568508
ba41482
1e78553
8b268ff
22ff46c
d4060ca
a90bb5a
f2d560a
21596ae
38e7871
c6cda6f
7a0683f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { render } from '@testing-library/react' | ||
import React from 'react' | ||
|
||
import { Input } from '@/components/input' | ||
|
||
describe('Input', () => { | ||
it('should default to `type="text"`', () => { | ||
const { queryByLabelText } = render(<Input aria-label="test input" />) | ||
|
||
expect(queryByLabelText('test input')).toBeInTheDocument() | ||
expect(queryByLabelText('test input')).toHaveAttribute('type', 'text') | ||
}) | ||
|
||
it('should not have the `disabled` attribute and `aria-disabled="false"` if `loading` is false', () => { | ||
const { queryByLabelText } = render(<Input aria-label="test input" />) | ||
|
||
expect(queryByLabelText('test input')).toBeInTheDocument() | ||
expect(queryByLabelText('test input')).not.toHaveAttribute('disabled') | ||
expect(queryByLabelText('test input')).toHaveAttribute('aria-disabled', 'false') | ||
expect(queryByLabelText('test input')).not.toBeDisabled() | ||
}) | ||
|
||
it('should have the `border-base` class by default', () => { | ||
const { queryByLabelText } = render(<Input aria-label="test input" />) | ||
|
||
expect(queryByLabelText('test input')).toBeInTheDocument() | ||
expect(queryByLabelText('test input')).toHaveClass('border-base') | ||
}) | ||
|
||
it('should have the `pl-12` class when the `icon` variant is passed', () => { | ||
const { queryByLabelText } = render(<Input aria-label="test input" icon={<div />} />) | ||
|
||
expect(queryByLabelText('test input')).toBeInTheDocument() | ||
expect(queryByLabelText('test input')).toHaveClass('pl-12') | ||
}) | ||
|
||
it('should have the `bg-disabled` and `border-transparent` classes when the `disabled` variant is passed', () => { | ||
const { queryByLabelText } = render(<Input aria-label="test input" disabled />) | ||
|
||
expect(queryByLabelText('test input')).toBeInTheDocument() | ||
expect(queryByLabelText('test input')).toHaveClass('bg-disabled') | ||
expect(queryByLabelText('test input')).toHaveClass('border-transparent') | ||
}) | ||
|
||
it('should have the `aria-invalid` and `aria-describedby` attributes if errorMessage is present', () => { | ||
const { queryByLabelText, queryByText } = render( | ||
<Input aria-label="test input" errorMessage="some error" />, | ||
) | ||
|
||
expect(queryByLabelText('test input')).toBeInTheDocument() | ||
expect(queryByLabelText('test input')).toHaveAttribute('aria-invalid') | ||
expect(queryByLabelText('test input')).toHaveAttribute('aria-describedby') | ||
expect(queryByText('some error')).toBeInTheDocument() | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { type VariantProps, cva } from 'class-variance-authority' | ||
import React, { forwardRef } from 'react' | ||
|
||
import { cn } from '@/utils/cn' | ||
|
||
const inputVariants = cva( | ||
[ | ||
'w-full h-14 rounded-xl border border-2 px-4 text-base text-medium', | ||
'focus:outline-none focus:border-focus', | ||
'placeholder-disabled', | ||
], | ||
|
||
{ | ||
variants: { | ||
variant: { | ||
default: 'border-base', | ||
}, | ||
disabled: { | ||
true: 'bg-disabled border-transparent', | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: 'default', | ||
}, | ||
}, | ||
) | ||
|
||
export interface InputProps | ||
extends VariantProps<typeof inputVariants>, | ||
React.InputHTMLAttributes<HTMLInputElement> { | ||
errorMessage?: string | ||
disabled?: boolean | ||
icon?: React.ReactNode | ||
} | ||
|
||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input( | ||
{ type = 'text', icon, errorMessage, disabled, className, ...props }, | ||
ref, | ||
) { | ||
return ( | ||
<div className="relative"> | ||
{icon && <div className="absolute left-4 top-4">{icon}</div>} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we satisfied with the absolute positioning for the icon or should we inline it with the input? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine for now, I don't see yet an advantage of using it inline 🤔 |
||
<input | ||
ref={ref} | ||
type={type} | ||
className={cn(inputVariants({ disabled }), icon && 'pl-12', className)} | ||
disabled={disabled ?? false} | ||
aria-disabled={disabled ?? false} | ||
aria-invalid={!!errorMessage || undefined} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that |
||
aria-describedby={errorMessage} | ||
{...props} | ||
/> | ||
{errorMessage && ( | ||
<div className="absolute top-full left-0 right-0 text-error text-sm px-2"> | ||
{errorMessage} | ||
</div> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not set absolute position to the error message. A simple |
||
)} | ||
</div> | ||
) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { type VariantProps, cva } from 'class-variance-authority' | ||
import React, { forwardRef } from 'react' | ||
|
||
import { cn } from '@/utils/cn' | ||
|
||
const labelVariants = cva('text-medium font-medium leading-6 px-2 flex items-center gap-2') | ||
|
||
export interface LabelProps | ||
extends VariantProps<typeof labelVariants>, | ||
React.LabelHTMLAttributes<HTMLLabelElement> { | ||
children: React.ReactNode | ||
} | ||
|
||
export const Label = forwardRef<HTMLLabelElement, LabelProps>(function Label( | ||
{ className, children, ...props }, | ||
ref, | ||
) { | ||
return ( | ||
<label ref={ref} className={cn(labelVariants(), className)} {...props}> | ||
{children} | ||
</label> | ||
) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we pass an error message we need to:
aria-invalid
andaria-describedby
attributes to the inputAdditionally we can use the label component directly, by adding a
label
prop:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we must use it separately because there is a situation when we have also a link besides the actualy label (as per graphic)
e.g. Label link