Validaciones mandatorias — Sección 8 — IDs concretos
This commit is contained in:
109
components/consent-screen.tsx
Normal file
109
components/consent-screen.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import type { BrandConfig } from '@/lib/types'
|
||||
|
||||
interface ConsentScreenProps {
|
||||
brandConfig: BrandConfig | null
|
||||
onAccept: (consentOperativo: boolean, consentMacroN1: boolean) => void
|
||||
}
|
||||
|
||||
export function ConsentScreen({ brandConfig, onAccept }: ConsentScreenProps) {
|
||||
const [operativo, setOperativo] = useState(false)
|
||||
const [macroN1, setMacroN1] = useState(false)
|
||||
|
||||
const orgName = brandConfig?.org_name ?? 'tu empresa'
|
||||
|
||||
return (
|
||||
<div className="consent-container" role="main">
|
||||
<h1 className="consent-title">Antes de comenzar</h1>
|
||||
|
||||
<section className="consent-section" aria-labelledby="consent-intro-heading">
|
||||
<h2 id="consent-intro-heading" className="consent-section-title">
|
||||
¿Qué vamos a hacer con tus respuestas?
|
||||
</h2>
|
||||
<p>
|
||||
Esta encuesta tiene dos propósitos que podés autorizar por separado:
|
||||
</p>
|
||||
<ol className="consent-list">
|
||||
<li>
|
||||
<strong>Diagnóstico interno de {orgName}:</strong> tus respuestas ayudan a la empresa
|
||||
a entender la realidad de las personas que cuidan a familiares con discapacidad o adultos
|
||||
mayores, para diseñar apoyos más adecuados. Solo se trabaja con{' '}
|
||||
<strong>datos agregados</strong> — nunca accede a respuestas individuales.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Estudio país (opcional):</strong> tus respuestas anonimizadas contribuyen a un
|
||||
mapeo nacional sobre la economía del cuidado en Paraguay. Los datos se procesan de
|
||||
forma irreversiblemente desvinculada de cualquier empresa o persona.
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Párrafo OBLIGATORIO: anonimato y no-revocación (ADR-003) */}
|
||||
<section className="consent-anon-box" role="note" aria-label="Sobre el anonimato">
|
||||
<p>
|
||||
<strong>Esta encuesta es completamente anónima.</strong> No guardamos ningún dato que
|
||||
te identifique (nombre, número de documento, correo ni ningún otro identificador personal).
|
||||
Por ser anónima, <strong>no es posible revocar ni eliminar tu respuesta una vez enviada</strong>,
|
||||
ya que no habría forma de encontrarla entre las demás.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="consent-checkboxes" aria-labelledby="consent-choices-heading">
|
||||
<h2 id="consent-choices-heading" className="consent-section-title">
|
||||
Tus decisiones
|
||||
</h2>
|
||||
|
||||
{/* Checkbox 1: operativo — REQUERIDO */}
|
||||
<label className="consent-checkbox-label" htmlFor="consent-operativo">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="consent-operativo"
|
||||
checked={operativo}
|
||||
onChange={(e) => setOperativo(e.target.checked)}
|
||||
aria-required="true"
|
||||
className="consent-checkbox-input"
|
||||
/>
|
||||
<span className="consent-checkbox-text">
|
||||
<strong>Acepto</strong> que mis respuestas se usen para el diagnóstico interno de{' '}
|
||||
{orgName} sobre cuidado y discapacidad.{' '}
|
||||
<span className="consent-required-badge">Requerido para continuar</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Checkbox 2: macro_n1 — OPCIONAL */}
|
||||
<label className="consent-checkbox-label" htmlFor="consent-macro">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="consent-macro"
|
||||
checked={macroN1}
|
||||
onChange={(e) => setMacroN1(e.target.checked)}
|
||||
className="consent-checkbox-input"
|
||||
/>
|
||||
<span className="consent-checkbox-text">
|
||||
<strong>También acepto</strong> que mis respuestas anonimizadas contribuyan al estudio
|
||||
país sobre economía del cuidado (sin identificarme en ningún caso).{' '}
|
||||
<span className="consent-optional-badge">Opcional</span>
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div className="consent-footer">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!operativo}
|
||||
onClick={() => onAccept(operativo, macroN1)}
|
||||
className={`btn ${operativo ? 'btn-primary' : 'btn-primary btn-disabled'}`}
|
||||
aria-disabled={!operativo}
|
||||
>
|
||||
Comenzar la encuesta
|
||||
</button>
|
||||
{!operativo && (
|
||||
<p className="consent-required-hint" role="status" aria-live="polite">
|
||||
Necesitás marcar el primer checkbox para continuar.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { CheckboxGroup } from './widgets/checkbox-group'
|
||||
import { RatingScale } from './widgets/rating-scale'
|
||||
import { TextAreaField } from './widgets/text-area-field'
|
||||
import { TextShortField } from './widgets/text-short-field'
|
||||
import { GeoSelectWidget } from './widgets/geo-select-widget'
|
||||
|
||||
interface QuestionRendererProps {
|
||||
question: Question
|
||||
@@ -30,6 +31,22 @@ export function QuestionRenderer({
|
||||
const { code, type, prompt, required, max_select, options } = question
|
||||
const error = errors[code]
|
||||
|
||||
// A1 (Departamento) renders a combined geo widget that also handles A2 (Ciudad)
|
||||
if (code === 'A1') {
|
||||
return (
|
||||
<GeoSelectWidget
|
||||
departmentValue={(answers['A1'] as string) ?? ''}
|
||||
cityValue={(answers['A2'] as string) ?? ''}
|
||||
onDepartmentChange={(v) => onChange('A1', v)}
|
||||
onCityChange={(v) => onChange('A2', v)}
|
||||
departmentError={errors['A1']}
|
||||
cityError={errors['A2']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
// A2 is rendered inside the GeoSelectWidget above — skip here to avoid duplication
|
||||
if (code === 'A2') return null
|
||||
|
||||
if (type === 'single_choice' && isArrayOptions(options)) {
|
||||
return (
|
||||
<RadioGroup
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from 'react'
|
||||
import type { FormAnswers, Question, Survey } from '@/lib/types'
|
||||
import { clearDraft, loadDraft, saveDraft } from '@/lib/storage'
|
||||
import { getHiddenCodes, isQuestionVisible } from '@/lib/conditional'
|
||||
import { submitSurvey } from '@/app/actions/submit-survey'
|
||||
import { ConsentScreen } from './consent-screen'
|
||||
import { ProgressBar } from './progress-bar'
|
||||
import { QuestionRenderer } from './question-renderer'
|
||||
import { SummaryScreen } from './summary-screen'
|
||||
|
||||
// Section metadata (code prefix → display title)
|
||||
// Prefixes: 'A' for A1-A7, '1' for 1.1-1.16, '2' for 2.1-2.5, etc.
|
||||
const SECTIONS: { prefix: string; title: string; sensitive?: boolean }[] = [
|
||||
{ prefix: 'A', title: 'Datos Generales' },
|
||||
{ prefix: '1', title: 'Prevalencia: cuidado y discapacidad', sensitive: true },
|
||||
@@ -20,29 +20,29 @@ const SECTIONS: { prefix: string; title: string; sensitive?: boolean }[] = [
|
||||
{ prefix: '7', title: 'Para cerrar' },
|
||||
]
|
||||
|
||||
// Extracts the leading alpha or numeric segment from a question code.
|
||||
// 'A1' → 'A', '1.1' → '1', '1.16' → '1', '6.4' → '6', '7.1' → '7'
|
||||
function getSectionPrefix(code: string): string {
|
||||
const m = code.match(/^([A-Za-z]+|\d+)/)
|
||||
return m ? m[1] : code
|
||||
}
|
||||
|
||||
type FormStage = 'consent' | 'form' | 'success' | 'error'
|
||||
|
||||
interface SurveyFormProps {
|
||||
survey: Survey
|
||||
questions: Question[]
|
||||
}
|
||||
|
||||
export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
const [stage, setStage] = useState<FormStage>('consent')
|
||||
const [consentOperativo, setConsentOperativo] = useState(false)
|
||||
const [consentMacroN1, setConsentMacroN1] = useState(false)
|
||||
const [answers, setAnswers] = useState<FormAnswers>({})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [hydrated, setHydrated] = useState(false)
|
||||
|
||||
// Announce when conditional blocks appear/disappear
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [a11yAnnounce, setA11yAnnounce] = useState('')
|
||||
|
||||
// Track which trigger codes had "Sí" on prev render to detect toggle
|
||||
const prevVisibility = useRef<Record<string, boolean>>({})
|
||||
const [hydrated, setHydrated] = useState(false)
|
||||
const prevAnswers = useRef<FormAnswers>({})
|
||||
|
||||
// Hydrate from localStorage on mount
|
||||
useEffect(() => {
|
||||
@@ -51,43 +51,34 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
setHydrated(true)
|
||||
}, [survey.id])
|
||||
|
||||
// Persist to localStorage on every change (after hydration)
|
||||
// Persist to localStorage after hydration
|
||||
useEffect(() => {
|
||||
if (!hydrated) return
|
||||
saveDraft(survey.id, answers)
|
||||
prevAnswers.current = answers
|
||||
}, [answers, hydrated, survey.id])
|
||||
|
||||
const handleChange = useCallback(
|
||||
(code: string, value: FormAnswers[string]) => {
|
||||
setAnswers((prev) => {
|
||||
const updated = { ...prev, [code]: value }
|
||||
|
||||
// Clear any dependent questions that become hidden
|
||||
const toHide = getHiddenCodes(code, value, questions, prev)
|
||||
for (const hiddenCode of toHide) {
|
||||
updated[hiddenCode] = undefined as unknown as FormAnswers[string]
|
||||
}
|
||||
|
||||
// Announce block visibility change to screen readers
|
||||
if (toHide.length > 0) {
|
||||
setA11yAnnounce('Se ocultaron preguntas relacionadas y se limpiaron sus respuestas.')
|
||||
} else {
|
||||
// Check if a conditional block just became visible
|
||||
const appeared = questions.some(
|
||||
(q) =>
|
||||
q.conditional_rules?.some((r) => r.if_question === code) &&
|
||||
isQuestionVisible(q, updated) &&
|
||||
!isQuestionVisible(q, prev)
|
||||
)
|
||||
if (appeared) {
|
||||
setA11yAnnounce('Aparecieron nuevas preguntas según tu respuesta.')
|
||||
}
|
||||
if (appeared) setA11yAnnounce('Aparecieron nuevas preguntas según tu respuesta.')
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
// Clear validation error for this field
|
||||
setErrors((prev) => {
|
||||
if (!prev[code]) return prev
|
||||
const { [code]: _, ...rest } = prev
|
||||
@@ -103,12 +94,7 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
if (!q.required) continue
|
||||
if (!isQuestionVisible(q, answers)) continue
|
||||
const val = answers[q.code]
|
||||
if (
|
||||
val === null ||
|
||||
val === undefined ||
|
||||
val === '' ||
|
||||
(Array.isArray(val) && val.length === 0)
|
||||
) {
|
||||
if (val === null || val === undefined || val === '' || (Array.isArray(val) && val.length === 0)) {
|
||||
newErrors[q.code] = 'Este campo es obligatorio.'
|
||||
}
|
||||
}
|
||||
@@ -118,82 +104,129 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (isPending) return
|
||||
if (!validate()) {
|
||||
// Move focus to first error
|
||||
const firstErrorCode = Object.keys(errors)[0]
|
||||
if (firstErrorCode) {
|
||||
document.getElementById(firstErrorCode)?.focus()
|
||||
}
|
||||
if (firstErrorCode) document.getElementById(firstErrorCode)?.focus()
|
||||
return
|
||||
}
|
||||
// 2A: no write to DB — just show summary
|
||||
clearDraft(survey.id)
|
||||
setSubmitted(true)
|
||||
|
||||
const questionMap = Object.fromEntries(questions.map((q) => [q.code, q.id]))
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await submitSurvey({
|
||||
surveyId: survey.id,
|
||||
answers: answers as Record<string, unknown>,
|
||||
questionMap,
|
||||
consentOperativo,
|
||||
consentMacroN1,
|
||||
})
|
||||
if (result.success) {
|
||||
clearDraft(survey.id)
|
||||
setStage('success')
|
||||
} else {
|
||||
setSubmitError(result.error)
|
||||
setStage('error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleStartOver() {
|
||||
setAnswers({})
|
||||
setSubmitted(false)
|
||||
setErrors({})
|
||||
// ── Stage: consent ──────────────────────────────────────────
|
||||
if (stage === 'consent') {
|
||||
return (
|
||||
<ConsentScreen
|
||||
brandConfig={survey.brand_config}
|
||||
onAccept={(op, macro) => {
|
||||
setConsentOperativo(op)
|
||||
setConsentMacroN1(macro)
|
||||
setStage('form')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Visible questions driven by conditional_rules
|
||||
// ── Stage: success ──────────────────────────────────────────
|
||||
if (stage === 'success') {
|
||||
return (
|
||||
<div className="survey-wrapper">
|
||||
<div className="thank-you-container" role="main">
|
||||
<h1 className="thank-you-title">¡Gracias por participar!</h1>
|
||||
<p className="thank-you-body">
|
||||
Tu respuesta fue registrada correctamente. Recordá que la encuesta es anónima: no hay
|
||||
forma de vincular tus respuestas con tu identidad.
|
||||
</p>
|
||||
<p className="thank-you-body">
|
||||
Los resultados agregados ayudarán a {survey.brand_config?.org_name ?? 'tu empresa'} a
|
||||
diseñar mejores apoyos para las personas cuidadoras.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Stage: error ────────────────────────────────────────────
|
||||
if (stage === 'error') {
|
||||
return (
|
||||
<div className="survey-wrapper">
|
||||
<div className="error-container" role="main">
|
||||
<h1 className="error-title">No se pudo enviar la encuesta</h1>
|
||||
<p>Hubo un error al guardar tus respuestas ({submitError}). Podés intentarlo de nuevo.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => { setSubmitError(null); setStage('form') }}
|
||||
>
|
||||
Volver e intentar de nuevo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Stage: form ─────────────────────────────────────────────
|
||||
const visibleQuestions = questions.filter((q) => isQuestionVisible(q, answers))
|
||||
const answeredCount = visibleQuestions.filter((q) => {
|
||||
const v = answers[q.code]
|
||||
return v !== null && v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)
|
||||
}).length
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<SummaryScreen
|
||||
surveyTitle={survey.title}
|
||||
questions={questions}
|
||||
answers={answers}
|
||||
onStartOver={handleStartOver}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Group visible questions by section prefix, in order
|
||||
type Section = { prefix: string; title: string; sensitive?: boolean; questions: Question[] }
|
||||
const sectionMap = new Map<string, Section>()
|
||||
for (const q of visibleQuestions) {
|
||||
const prefix = getSectionPrefix(q.code)
|
||||
if (!sectionMap.has(prefix)) {
|
||||
const meta = SECTIONS.find((s) => s.prefix === prefix)
|
||||
sectionMap.set(prefix, {
|
||||
prefix,
|
||||
title: meta?.title ?? prefix,
|
||||
sensitive: meta?.sensitive,
|
||||
questions: [],
|
||||
})
|
||||
sectionMap.set(prefix, { prefix, title: meta?.title ?? prefix, sensitive: meta?.sensitive, questions: [] })
|
||||
}
|
||||
sectionMap.get(prefix)!.questions.push(q)
|
||||
}
|
||||
|
||||
const bc = survey.brand_config
|
||||
|
||||
return (
|
||||
<div className="survey-wrapper">
|
||||
{/* Skip link for keyboard users */}
|
||||
<a href="#survey-form" className="skip-link">
|
||||
Ir al formulario
|
||||
</a>
|
||||
<a href="#survey-form" className="skip-link">Ir al formulario</a>
|
||||
|
||||
{/* Screen-reader live region for conditional announcements */}
|
||||
<div
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="sr-only"
|
||||
role="status"
|
||||
>
|
||||
<div aria-live="polite" aria-atomic="true" className="sr-only" role="status">
|
||||
{a11yAnnounce}
|
||||
</div>
|
||||
|
||||
{/* Header contextual con org/branch si está configurado */}
|
||||
{bc?.org_name && (
|
||||
<div className="brand-header" role="note" aria-label="Información de la empresa">
|
||||
<p className="brand-text">
|
||||
Estás respondiendo esto porque pertenecés a <strong>{bc.org_name}</strong>
|
||||
{bc.branch_name && (
|
||||
<> y participás en las oficinas de <strong>{bc.branch_name}</strong>
|
||||
{bc.branch_city && <> en <strong>{bc.branch_city}</strong></>}.</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="survey-header">
|
||||
<h1 className="survey-title">{survey.title}</h1>
|
||||
<p className="survey-anon-note">
|
||||
Esta encuesta es anónima. Tus respuestas no te identifican.
|
||||
</p>
|
||||
<p className="survey-anon-note">Esta encuesta es anónima. Tus respuestas no te identifican.</p>
|
||||
</header>
|
||||
|
||||
<div className="survey-progress-sticky" aria-label="Barra de progreso">
|
||||
@@ -201,11 +234,7 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
</div>
|
||||
|
||||
<main id="survey-form">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
aria-label={`Encuesta: ${survey.title}`}
|
||||
>
|
||||
<form onSubmit={handleSubmit} noValidate aria-label={`Encuesta: ${survey.title}`}>
|
||||
{Array.from(sectionMap.values()).map((section) => (
|
||||
<section
|
||||
key={section.prefix}
|
||||
@@ -213,9 +242,7 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
aria-labelledby={`section-${section.prefix}`}
|
||||
>
|
||||
<div className="section-header">
|
||||
<h2 id={`section-${section.prefix}`} className="section-title">
|
||||
{section.title}
|
||||
</h2>
|
||||
<h2 id={`section-${section.prefix}`} className="section-title">{section.title}</h2>
|
||||
{section.sensitive && (
|
||||
<p className="section-sensitive-note" role="note">
|
||||
Esta sección contiene preguntas sensibles sobre salud o discapacidad.
|
||||
@@ -223,13 +250,8 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{section.questions.map((q) => (
|
||||
<div
|
||||
key={q.id}
|
||||
className="question-wrapper"
|
||||
data-code={q.code}
|
||||
>
|
||||
<div key={q.id} className="question-wrapper" data-code={q.code}>
|
||||
<QuestionRenderer
|
||||
question={q}
|
||||
answers={answers}
|
||||
@@ -242,8 +264,13 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
))}
|
||||
|
||||
<div className="form-footer">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Ver resumen de respuestas
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={isPending}
|
||||
aria-disabled={isPending}
|
||||
>
|
||||
{isPending ? 'Enviando…' : 'Enviar encuesta'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
90
components/widgets/geo-select-widget.tsx
Normal file
90
components/widgets/geo-select-widget.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
'use client'
|
||||
import { DEPARTMENTS, getCities } from '@/lib/paraguay-geo'
|
||||
|
||||
interface GeoSelectWidgetProps {
|
||||
departmentValue: string
|
||||
cityValue: string
|
||||
onDepartmentChange: (value: string) => void
|
||||
onCityChange: (value: string) => void
|
||||
departmentError?: string
|
||||
cityError?: string
|
||||
}
|
||||
|
||||
export function GeoSelectWidget({
|
||||
departmentValue,
|
||||
cityValue,
|
||||
onDepartmentChange,
|
||||
onCityChange,
|
||||
departmentError,
|
||||
cityError,
|
||||
}: GeoSelectWidgetProps) {
|
||||
const cities = departmentValue ? getCities(departmentValue) : []
|
||||
const cityDisabled = !departmentValue
|
||||
|
||||
function handleDepartmentChange(val: string) {
|
||||
onDepartmentChange(val)
|
||||
onCityChange('') // limpiar ciudad al cambiar departamento
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="geo-select-wrapper">
|
||||
{/* Departamento */}
|
||||
<div className="geo-select-group">
|
||||
<label className="question-legend" htmlFor="A1">
|
||||
Departamento
|
||||
</label>
|
||||
<select
|
||||
id="A1"
|
||||
name="A1"
|
||||
value={departmentValue}
|
||||
onChange={(e) => handleDepartmentChange(e.target.value)}
|
||||
aria-invalid={!!departmentError}
|
||||
aria-describedby={departmentError ? 'A1-error' : undefined}
|
||||
className="geo-select"
|
||||
>
|
||||
<option value="">Seleccioná un departamento…</option>
|
||||
{DEPARTMENTS.map((dept) => (
|
||||
<option key={dept} value={dept}>{dept}</option>
|
||||
))}
|
||||
</select>
|
||||
{departmentError && (
|
||||
<p role="alert" className="field-error" id="A1-error">{departmentError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ciudad */}
|
||||
<div className="geo-select-group">
|
||||
<label
|
||||
className={`question-legend${cityDisabled ? ' geo-label-disabled' : ''}`}
|
||||
htmlFor="A2"
|
||||
>
|
||||
Ciudad
|
||||
{cityDisabled && (
|
||||
<span className="geo-disabled-hint"> (seleccioná un departamento primero)</span>
|
||||
)}
|
||||
</label>
|
||||
<select
|
||||
id="A2"
|
||||
name="A2"
|
||||
value={cityValue}
|
||||
onChange={(e) => onCityChange(e.target.value)}
|
||||
disabled={cityDisabled}
|
||||
aria-disabled={cityDisabled}
|
||||
aria-invalid={!!cityError}
|
||||
aria-describedby={cityError ? 'A2-error' : undefined}
|
||||
className={`geo-select${cityDisabled ? ' geo-select-disabled' : ''}`}
|
||||
>
|
||||
<option value="">
|
||||
{cityDisabled ? 'Primero seleccioná el departamento' : 'Seleccioná una ciudad…'}
|
||||
</option>
|
||||
{cities.map((city) => (
|
||||
<option key={city} value={city}>{city}</option>
|
||||
))}
|
||||
</select>
|
||||
{cityError && (
|
||||
<p role="alert" className="field-error" id="A2-error">{cityError}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user