+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@indec/react-commons",
"version": "7.1.1",
"version": "7.2.0",
"description": "Common reactjs components for apps",
"private": false,
"main": "index.js",
Expand Down
73 changes: 73 additions & 0 deletions src/__tests__/components/Select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,77 @@ describe('<Select>', () => {
expect(getByText(container, 'No hay opciones')).toBeInTheDocument();
});
});

describe('when label is provided', () => {
beforeEach(() => {
props.label = 'Test Label';
});

it('should display the label', () => {
const {container} = getComponent();
expect(getByText(container, 'Test Label')).toBeInTheDocument();
});

it('should associate label with input', () => {
const {container} = getComponent();
const label = getByText(container, 'Test Label');
const input = container.querySelector('input');
expect(label).toHaveAttribute('for', props.name);
expect(input).toHaveAttribute('id', props.name);
});
});

describe('when error is provided', () => {
beforeEach(() => {
props.error = 'This field is required';
});

it('should display error message when dropdown is closed', () => {
const {container} = getComponent();
expect(getByText(container, 'This field is required')).toBeInTheDocument();
});

it('should hide error message when dropdown is open', () => {
const {container} = getComponent();
const input = container.querySelector('input');

expect(getByText(container, 'This field is required')).toBeInTheDocument();

fireEvent.click(input);

expect(queryByText(container, 'This field is required')).toBeNull();
});

it('should show error message again when dropdown is closed', () => {
const {container} = getComponent();
const input = container.querySelector('input');

fireEvent.click(input);
expect(queryByText(container, 'This field is required')).toBeNull();

const backdrop = container.querySelector('.fixed');
fireEvent.click(backdrop);

expect(getByText(container, 'This field is required')).toBeInTheDocument();
});

it('should apply error styling to input', () => {
const {container} = getComponent();
const input = container.querySelector('input');
expect(input).toHaveClass('border-error');
});
});

describe('when both label and error are provided', () => {
beforeEach(() => {
props.label = 'Country';
props.error = 'Please select a country';
});

it('should display both label and error', () => {
const {container} = getComponent();
expect(getByText(container, 'Country')).toBeInTheDocument();
expect(getByText(container, 'Please select a country')).toBeInTheDocument();
});
});
});
54 changes: 35 additions & 19 deletions src/components/Select.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {ChevronDownIcon} from './Icons';
import ErrorMessage from './ErrorMessage';

export default function Select({
options = [],
Expand All @@ -10,7 +11,9 @@ export default function Select({
keyValue = 'value',
name,
value,
onSelect
onSelect,
label,
error
}) {
const [isOpen, setIsOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState('');
Expand All @@ -24,30 +27,43 @@ export default function Select({
const selectedValue = React.useMemo(() => options.find(option => option[keyValue] === value) || {}, [value]);

const filteredOptions = React.useMemo(() => {
if (!searchTerm) return options;
if (!searchTerm) {
return options;
}
return options.filter(option => option.label?.toLowerCase().includes(searchTerm.toLowerCase()));
}, [options, searchTerm]);

return (
<div className="w-full relative">
<div className="relative">
<input
type="text"
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white cursor-pointer'
}`}
placeholder={placeholder}
value={selectedValue.label || ''}
onClick={() => !disabled && setIsOpen(!isOpen)}
onChange={e => {
setSearchTerm(e.target.value);
setIsOpen(true);
}}
disabled={disabled}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
<div className="w-full">
{label && (
<label htmlFor={name} className="block text-[17px] text-black text-xl font-medium">
{label}
</label>
)}
<div className="relative">
<input
id={name}
name={name}
type="text"
aria-label={label}
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 ${
error ? 'border-error focus:ring-error' : 'border-gray-300 focus:ring-primary'
} ${disabled ? 'bg-gray-100 cursor-not-allowed' : ''}`}
placeholder={placeholder}
value={selectedValue.label || ''}
onClick={() => !disabled && setIsOpen(!isOpen)}
onChange={e => {
setSearchTerm(e.target.value);
setIsOpen(true);
}}
disabled={disabled}
/>
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
</div>
</div>
{error && !isOpen && <ErrorMessage error={error} />}
</div>

{isOpen && (
Expand Down
16 changes: 16 additions & 0 deletions src/stories/Select.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,20 @@ export const Searchable = {
],
placeholder: 'Type to search fruits...'
}
};

export const WithError = {
args: {
label: 'Country',
error: 'Please select a valid country',
placeholder: 'Select a country...'
}
};

export const WithLabelAndError = {
args: {
label: 'Product Category',
value: 2,
error: 'This category is not available in your region'
}
};
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载