Skip to content

Commit 4a548fd

Browse files
feat(select): add label and error props (#113)
1 parent d72f111 commit 4a548fd

4 files changed

Lines changed: 125 additions & 20 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@indec/react-commons",
3-
"version": "7.1.1",
3+
"version": "7.2.0",
44
"description": "Common reactjs components for apps",
55
"private": false,
66
"main": "index.js",

src/__tests__/components/Select.test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,77 @@ describe('<Select>', () => {
112112
expect(getByText(container, 'No hay opciones')).toBeInTheDocument();
113113
});
114114
});
115+
116+
describe('when label is provided', () => {
117+
beforeEach(() => {
118+
props.label = 'Test Label';
119+
});
120+
121+
it('should display the label', () => {
122+
const {container} = getComponent();
123+
expect(getByText(container, 'Test Label')).toBeInTheDocument();
124+
});
125+
126+
it('should associate label with input', () => {
127+
const {container} = getComponent();
128+
const label = getByText(container, 'Test Label');
129+
const input = container.querySelector('input');
130+
expect(label).toHaveAttribute('for', props.name);
131+
expect(input).toHaveAttribute('id', props.name);
132+
});
133+
});
134+
135+
describe('when error is provided', () => {
136+
beforeEach(() => {
137+
props.error = 'This field is required';
138+
});
139+
140+
it('should display error message when dropdown is closed', () => {
141+
const {container} = getComponent();
142+
expect(getByText(container, 'This field is required')).toBeInTheDocument();
143+
});
144+
145+
it('should hide error message when dropdown is open', () => {
146+
const {container} = getComponent();
147+
const input = container.querySelector('input');
148+
149+
expect(getByText(container, 'This field is required')).toBeInTheDocument();
150+
151+
fireEvent.click(input);
152+
153+
expect(queryByText(container, 'This field is required')).toBeNull();
154+
});
155+
156+
it('should show error message again when dropdown is closed', () => {
157+
const {container} = getComponent();
158+
const input = container.querySelector('input');
159+
160+
fireEvent.click(input);
161+
expect(queryByText(container, 'This field is required')).toBeNull();
162+
163+
const backdrop = container.querySelector('.fixed');
164+
fireEvent.click(backdrop);
165+
166+
expect(getByText(container, 'This field is required')).toBeInTheDocument();
167+
});
168+
169+
it('should apply error styling to input', () => {
170+
const {container} = getComponent();
171+
const input = container.querySelector('input');
172+
expect(input).toHaveClass('border-error');
173+
});
174+
});
175+
176+
describe('when both label and error are provided', () => {
177+
beforeEach(() => {
178+
props.label = 'Country';
179+
props.error = 'Please select a country';
180+
});
181+
182+
it('should display both label and error', () => {
183+
const {container} = getComponent();
184+
expect(getByText(container, 'Country')).toBeInTheDocument();
185+
expect(getByText(container, 'Please select a country')).toBeInTheDocument();
186+
});
187+
});
115188
});

src/components/Select.jsx

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import {ChevronDownIcon} from './Icons';
3+
import ErrorMessage from './ErrorMessage';
34

45
export default function Select({
56
options = [],
@@ -10,7 +11,9 @@ export default function Select({
1011
keyValue = 'value',
1112
name,
1213
value,
13-
onSelect
14+
onSelect,
15+
label,
16+
error
1417
}) {
1518
const [isOpen, setIsOpen] = React.useState(false);
1619
const [searchTerm, setSearchTerm] = React.useState('');
@@ -24,30 +27,43 @@ export default function Select({
2427
const selectedValue = React.useMemo(() => options.find(option => option[keyValue] === value) || {}, [value]);
2528

2629
const filteredOptions = React.useMemo(() => {
27-
if (!searchTerm) return options;
30+
if (!searchTerm) {
31+
return options;
32+
}
2833
return options.filter(option => option.label?.toLowerCase().includes(searchTerm.toLowerCase()));
2934
}, [options, searchTerm]);
3035

3136
return (
3237
<div className="w-full relative">
33-
<div className="relative">
34-
<input
35-
type="text"
36-
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
37-
disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white cursor-pointer'
38-
}`}
39-
placeholder={placeholder}
40-
value={selectedValue.label || ''}
41-
onClick={() => !disabled && setIsOpen(!isOpen)}
42-
onChange={e => {
43-
setSearchTerm(e.target.value);
44-
setIsOpen(true);
45-
}}
46-
disabled={disabled}
47-
/>
48-
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
49-
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
38+
<div className="w-full">
39+
{label && (
40+
<label htmlFor={name} className="block text-[17px] text-black text-xl font-medium">
41+
{label}
42+
</label>
43+
)}
44+
<div className="relative">
45+
<input
46+
id={name}
47+
name={name}
48+
type="text"
49+
aria-label={label}
50+
className={`w-full px-4 py-2 pr-10 border-2 border-gray-400 rounded-lg bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 ${
51+
error ? 'border-error focus:ring-error' : 'border-gray-300 focus:ring-primary'
52+
} ${disabled ? 'bg-gray-100 cursor-not-allowed' : ''}`}
53+
placeholder={placeholder}
54+
value={selectedValue.label || ''}
55+
onClick={() => !disabled && setIsOpen(!isOpen)}
56+
onChange={e => {
57+
setSearchTerm(e.target.value);
58+
setIsOpen(true);
59+
}}
60+
disabled={disabled}
61+
/>
62+
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
63+
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
64+
</div>
5065
</div>
66+
{error && !isOpen && <ErrorMessage error={error} />}
5167
</div>
5268

5369
{isOpen && (

src/stories/Select.stories.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,20 @@ export const Searchable = {
126126
],
127127
placeholder: 'Type to search fruits...'
128128
}
129+
};
130+
131+
export const WithError = {
132+
args: {
133+
label: 'Country',
134+
error: 'Please select a valid country',
135+
placeholder: 'Select a country...'
136+
}
137+
};
138+
139+
export const WithLabelAndError = {
140+
args: {
141+
label: 'Product Category',
142+
value: 2,
143+
error: 'This category is not available in your region'
144+
}
129145
};

0 commit comments

Comments
 (0)