Skip to content
Open
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
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@
## 2025-06-16 - [Functional Matchers for Complex Buttons and Utility-First Layout]
**Learning:** React Testing Library's `findByText` can fail on buttons containing Materialize icons due to text node fragmentation and the icon's name being part of the `textContent`. Using a functional matcher that checks for partial content and tag name is more robust. Additionally, strictly adhering to utility classes (e.g., 'center-align') instead of inline styles ensures compliance with repository constraints and theme consistency.
**Action:** Always use functional matchers for testing interactive elements with icons and avoid inline styles by leveraging existing CSS utility classes.

## 2025-06-17 - [Accessible Checkboxes and Consistent Prop Spreading]
**Learning:** Enhancing base form components like `CustomCheckbox` with standardized `required` indicators (visual asterisk and `aria-required`) and grid support (`s`, `m`, `l`, `xl`, `offset`) improves both accessibility and developer productivity. Ensuring that `...props` are consistently applied to the inner `input` regardless of the presence of a wrapper `div` maintains a predictable component API.
**Action:** Always provide visual and semantic cues for mandatory fields and ensure consistent prop-spreading behavior in multi-layered components.
1 change: 0 additions & 1 deletion src/components/PlayerCasoContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ExamService from "../services/ExamService";
import { useHistory } from 'react-router-dom';
import { alertError, alertSuccess } from "../services/AlertService";
import CasoContext from "../context/CasoContext";
import { CustomButton } from "./custom";
import EnarmUtil from "../modules/EnarmUtil";
import ContributionTypeSelector from "./ContributionTypeSelector";
import ContributionsSummary from "./ContributionsSummary";
Expand Down
63 changes: 57 additions & 6 deletions src/components/custom/CustomCheckbox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import PropTypes from 'prop-types';
const CustomCheckbox = ({
id,
label,
checked = false, // Renamed from 'value' for clarity with checkbox 'checked' attribute
checked = false,
onChange,
disabled = false,
className = '', // Applied to the input element
labelClassName = '', // Applied to the label element
indeterminate = false,
value, // HTML value attribute, not for checked state
s,
m,
l,
xl,
offset,
required = false,
wrapperClassName = '',
...props
}) => {
const inputRef = useRef(null);
Expand All @@ -26,9 +33,26 @@ const CustomCheckbox = ({
inputClasses += ' indeterminate-checkbox';
}

// The main wrapper is the label for Materialize checkboxes
return (
<label htmlFor={id} className={labelClassName} {...props}>
// Construct wrapper classes for grid support
let wrapperClasses = wrapperClassName.trim();
const hasGrid = !!(s || m || l || xl || offset);
if (hasGrid) {
if (!wrapperClasses.includes('col')) {
wrapperClasses += ' col';
}
if (s) wrapperClasses += ` s${s}`;
if (m) wrapperClasses += ` m${m}`;
if (l) wrapperClasses += ` l${l}`;
if (xl) wrapperClasses += ` xl${xl}`;
if (offset) {
offset.split(' ').forEach(off => {
if (off) wrapperClasses += ` offset-${off}`;
});
}
}

const checkboxContent = (
<label htmlFor={id} className={labelClassName}>
<input
ref={inputRef}
type="checkbox"
Expand All @@ -37,11 +61,31 @@ const CustomCheckbox = ({
onChange={onChange}
disabled={disabled}
className={inputClasses.trim()}
value={value} // HTML value attribute
value={value}
aria-required={required ? 'true' : undefined}
{...props}
/>
<span>{label}</span>
<span>
{label}
{required && (
<span
className="red-text"
style={{ marginLeft: '4px', fontWeight: 'bold' }}
aria-hidden="true"
title="Obligatorio"
>
*
</span>
)}
</span>
</label>
);

if (hasGrid || wrapperClassName) {
return <div className={wrapperClasses.trim()}>{checkboxContent}</div>;
}

return checkboxContent;
};

CustomCheckbox.propTypes = {
Expand All @@ -54,6 +98,13 @@ CustomCheckbox.propTypes = {
labelClassName: PropTypes.string, // For the label element
indeterminate: PropTypes.bool,
value: PropTypes.string, // HTML value attribute for the checkbox
s: PropTypes.number,
m: PropTypes.number,
l: PropTypes.number,
xl: PropTypes.number,
offset: PropTypes.string,
required: PropTypes.bool,
wrapperClassName: PropTypes.string,
};

export default CustomCheckbox;
55 changes: 55 additions & 0 deletions src/components/custom/CustomCheckbox.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, test, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import CustomCheckbox from './CustomCheckbox';

describe('CustomCheckbox', () => {
test('renders label correctly', () => {
render(<CustomCheckbox id="test-checkbox" label="Check me" onChange={() => {}} />);
expect(screen.getByLabelText(/Check me/i)).toBeInTheDocument();
});

test('calls onChange when clicked', () => {
const handleChange = vi.fn();
render(<CustomCheckbox id="test-checkbox" label="Check me" onChange={handleChange} />);
const checkbox = screen.getByLabelText(/Check me/i);
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalled();
});

test('is disabled when the prop is true', () => {
render(<CustomCheckbox id="test-checkbox" label="Check me" disabled onChange={() => {}} />);
const checkbox = screen.getByLabelText(/Check me/i);
expect(checkbox).toBeDisabled();
});

test('renders required asterisk when required prop is true', () => {
render(<CustomCheckbox id="test-checkbox" label="Check me" required onChange={() => {}} />);
expect(screen.getByText('*')).toBeInTheDocument();
expect(screen.getByText('*')).toHaveClass('red-text');
expect(screen.getByLabelText(/Check me/i)).toHaveAttribute('aria-required', 'true');
});

test('applies grid classes to the wrapper div', () => {
const { container } = render(
<CustomCheckbox id="test-checkbox" label="Check me" s={12} m={6} offset="s0 m3" onChange={() => {}} />
);
const wrapper = container.firstChild;
expect(wrapper.tagName).toBe('DIV');
expect(wrapper).toHaveClass('col');
expect(wrapper).toHaveClass('s12');
expect(wrapper).toHaveClass('m6');
expect(wrapper).toHaveClass('offset-s0');
expect(wrapper).toHaveClass('offset-m3');
});

test('applies wrapperClassName to the wrapper div', () => {
const { container } = render(
<CustomCheckbox id="test-checkbox" label="Check me" wrapperClassName="extra-class" s={12} onChange={() => {}} />
);
const wrapper = container.firstChild;
expect(wrapper).toHaveClass('extra-class');
expect(wrapper).toHaveClass('col');
expect(wrapper).toHaveClass('s12');
});
});