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 Progress Indicators and V2 Navigation Polish]
**Learning:** For progress indicators to be truly accessible, they must include `role="progressbar"` along with `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` attributes, allowing screen readers to accurately report status. In side-navigation rails, ensuring decorative icons are marked with `aria-hidden="true"` and the active link has `aria-current="page"` significantly improves screen reader navigation. Additionally, maintaining localization consistency (e.g., 'Inicio' vs 'Home') in new UI versions (V2) is essential for a cohesive user experience.
**Action:** Always implement full ARIA attributes for progress components and audit new navigation structures for both accessibility markers and language consistency.
14 changes: 12 additions & 2 deletions src/components/custom/CustomProgressBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@ const CustomProgressBar = ({
wrapperColor = 'grey lighten-3',
className = '',
style = EMPTY_STYLE,
height
height,
'aria-label': ariaLabel
}) => {
const isIndeterminate = progress === undefined || progress === null;
const finalStyle = height ? { ...style, height } : style;

return (
<div className={`progress ${wrapperColor} ${className}`} style={finalStyle}>
<div
className={`progress ${wrapperColor} ${className}`}
style={finalStyle}
role="progressbar"
aria-label={ariaLabel}
aria-valuenow={!isIndeterminate ? Math.round(progress) : undefined}
aria-valuemin={!isIndeterminate ? 0 : undefined}
aria-valuemax={!isIndeterminate ? 100 : undefined}
>
<div
className={`${isIndeterminate ? 'indeterminate' : 'determinate'} ${color}`}
style={!isIndeterminate ? { width: `${progress}%` } : {}}
Expand All @@ -40,6 +49,7 @@ CustomProgressBar.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
height: PropTypes.string,
'aria-label': PropTypes.string,
};

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

describe('CustomProgressBar', () => {
it('renders correctly with progress and ARIA attributes', () => {
render(<CustomProgressBar progress={45} aria-label="Test Progress" />);

const progressbar = screen.getByRole('progressbar');
expect(progressbar).toBeInTheDocument();
expect(progressbar).toHaveAttribute('aria-label', 'Test Progress');
expect(progressbar).toHaveAttribute('aria-valuenow', '45');
expect(progressbar).toHaveAttribute('aria-valuemin', '0');
expect(progressbar).toHaveAttribute('aria-valuemax', '100');

const determinatePart = progressbar.querySelector('.determinate');
expect(determinatePart).toBeInTheDocument();
expect(determinatePart).toHaveStyle({ width: '45%' });
});

it('renders indeterminate state when progress is null', () => {
render(<CustomProgressBar progress={null} />);

const progressbar = screen.getByRole('progressbar');
expect(progressbar).toBeInTheDocument();
expect(progressbar).not.toHaveAttribute('aria-valuenow');

const indeterminatePart = progressbar.querySelector('.indeterminate');
expect(indeterminatePart).toBeInTheDocument();
});

it('rounds progress for aria-valuenow', () => {
render(<CustomProgressBar progress={67.8} />);

const progressbar = screen.getByRole('progressbar');
expect(progressbar).toHaveAttribute('aria-valuenow', '68');
});
});
34 changes: 21 additions & 13 deletions src/v2/components/V2Navi.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const V2Navi = () => {
const isActive = (path) => location.pathname === path;

const navItems = [
{ label: "Home", icon: "home", path: "/v2/dashboard" },
{ label: "Inicio", icon: "home", path: "/v2/dashboard" },
{ label: "Práctica", icon: "medical_services", path: "/v2/practica" },
{ label: "Contribuir", icon: "add_circle", path: "/v2/contribuir" },
{ label: "Mis Casos", icon: "history", path: "/v2/mis-contribuciones" },
Expand All @@ -14,20 +14,28 @@ const V2Navi = () => {
];

return (
<nav className="v2-nav-rail">
<nav className="v2-nav-rail" aria-label="Navegación principal">
<div className="v2-nav-brand" style={{ marginBottom: '40px' }}>
<i className="material-icons v2-text-primary" style={{ fontSize: '32px' }}>local_hospital</i>
<i className="material-icons v2-text-primary" style={{ fontSize: '32px' }} aria-hidden="true">
local_hospital
</i>
</div>
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`v2-nav-item ${isActive(item.path) ? 'active' : ''}`}
>
<i className="material-icons">{item.icon}</i>
<span>{item.label}</span>
</Link>
))}
{navItems.map((item) => {
const isLinkActive = isActive(item.path);
return (
<Link
key={item.path}
to={item.path}
className={`v2-nav-item ${isLinkActive ? 'active' : ''}`}
aria-current={isLinkActive ? 'page' : undefined}
>
<i className="material-icons" aria-hidden="true">
{item.icon}
</i>
<span>{item.label}</span>
</Link>
);
})}
</nav>
);
};
Expand Down
20 changes: 14 additions & 6 deletions src/v2/pages/V2Examen.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import ExamService from '../../services/ExamService';
import { CustomPreloader } from '../../components/custom';

const V2Examen = () => {
const { identificador } = useParams();
Expand Down Expand Up @@ -51,7 +52,14 @@ const V2Examen = () => {
setState(prev => ({ ...prev, showFeedback: true, isCorrect: correct }));
};

if (state.loading) return <div className="center-align">Cargando caso clínico...</div>;
if (state.loading) {
return (
<div className="center-align" style={{ padding: '100px' }}>
<CustomPreloader active color="green" size="big" />
<p className="v2-body-large" style={{ marginTop: '20px' }}>Cargando caso clínico...</p>
</div>
);
}

const { caso, selectedAnswer, showFeedback, isCorrect } = state;

Expand All @@ -61,7 +69,7 @@ const V2Examen = () => {
<span className="v2-body-large v2-text-primary">Reto de Sesión</span>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<span className="v2-body-large">+50 XP</span>
<i className="material-icons" style={{ fontSize: '20px' }}>timer</i>
<i className="material-icons" style={{ fontSize: '20px' }} aria-hidden="true">timer</i>
<span>14:22</span>
</div>
</div>
Expand Down Expand Up @@ -112,7 +120,7 @@ const V2Examen = () => {
style={{ width: 'auto', padding: '0 32px', height: '56px', borderRadius: '16px' }}
onClick={handleSubmit}
>
Enviar Respuesta <i className="material-icons" style={{ marginLeft: '8px' }}>check</i>
Enviar Respuesta <i className="material-icons" style={{ marginLeft: '8px' }} aria-hidden="true">check</i>
</button>
</div>
) : (
Expand All @@ -122,12 +130,12 @@ const V2Examen = () => {
color: isCorrect ? 'var(--md-sys-color-on-primary-container)' : 'var(--md-sys-color-on-error-container)',
marginBottom: '24px', display: 'flex', alignItems: 'center', gap: '12px'
}}>
<i className="material-icons">{isCorrect ? 'check_circle' : 'error'}</i>
<i className="material-icons" aria-hidden="true">{isCorrect ? 'check_circle' : 'error'}</i>
<span className="v2-title-large">{isCorrect ? '¡Correcto!' : 'Incorrecto'}</span>
</div>
<div className="v2-card" style={{ backgroundColor: '#fffbe6', border: '1px solid #ffe58f', marginBottom: '24px' }}>
<h4 className="v2-title-large" style={{ color: '#856404', marginBottom: '12px' }}>
<i className="material-icons" style={{ verticalAlign: 'middle', marginRight: '8px' }}>lightbulb</i>
<i className="material-icons" style={{ verticalAlign: 'middle', marginRight: '8px' }} aria-hidden="true">lightbulb</i>
Perla Médica: Explicación
</h4>
<p className="v2-body-large">
Expand All @@ -140,7 +148,7 @@ const V2Examen = () => {
style={{ width: 'auto', padding: '0 32px', height: '56px', borderRadius: '16px' }}
onClick={() => history.push('/v2/dashboard')}
>
Siguiente Caso <i className="material-icons" style={{ marginLeft: '8px' }}>arrow_forward</i>
Siguiente Caso <i className="material-icons" style={{ marginLeft: '8px' }} aria-hidden="true">arrow_forward</i>
</button>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/v2/pages/V2Landing.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { Link } from 'react-router-dom';
import '../styles/v2-theme.css';

Expand Down