@@ -11,7 +11,7 @@ import { Helmet } from "react-helmet";
1111import toast from "react-hot-toast" ;
1212import { useTranslation } from "react-i18next" ;
1313import { FaCog , FaDatabase , FaInfo } from "react-icons/fa" ;
14- import { startTransition , useEffect , useState } from "react" ;
14+ import { startTransition , useEffect , useRef , useState } from "react" ;
1515import { useNavigate , useParams } from "react-router-dom" ;
1616
1717import { HOME_ROUTE , ROUTES } from "../../router.tsx" ;
@@ -69,6 +69,10 @@ function EditChartPage() {
6969
7070 const [ hasUnsavedChanges , setHasUnsavedChanges ] = useState ( false ) ;
7171 const [ saveStatus , setSaveStatus ] = useState < string > ( "" ) ;
72+ // Announces step transitions to assistive tech (WCAG 1.3.2 / 3.3.2)
73+ const [ stepAnnouncement , setStepAnnouncement ] = useState < string > ( "" ) ;
74+ const seriesSelectorRef = useRef < HTMLDivElement > ( null ) ;
75+ const configurationHeadingRef = useRef < HTMLHeadingElement > ( null ) ;
7276 useUnsavedChanges ( hasUnsavedChanges , t ( `unsavedChanges` ) ) ;
7377
7478 // After the initial load completes, reset any dirty flag that child components
@@ -124,6 +128,21 @@ function EditChartPage() {
124128 } , [ paramId ] ) ;
125129
126130 const haveData = data && data [ 0 ] . length > 0 ? true : dataSource ? true : false ;
131+ const isConfigStep = state . matches ( "config" ) ;
132+
133+ // Announce step transitions and move keyboard focus so AT users learn that
134+ // the next interactive section just became available (WCAG 1.3.2, 3.3.2).
135+ useEffect ( ( ) => {
136+ if ( loading ) return ;
137+ if ( isConfigStep ) {
138+ setStepAnnouncement ( t ( `body.options.configuration.announcement` ) ) ;
139+ // Defer until the configuration section is rendered/opened.
140+ requestAnimationFrame ( ( ) => configurationHeadingRef . current ?. focus ( ) ) ;
141+ } else if ( currentData && ! haveData ) {
142+ setStepAnnouncement ( t ( `body.preview.seriesSelector.announcement` ) ) ;
143+ requestAnimationFrame ( ( ) => seriesSelectorRef . current ?. focus ( ) ) ;
144+ }
145+ } , [ isConfigStep , currentData , haveData , loading , t ] ) ;
127146
128147 function handleUpload ( d : any ) {
129148 setHasUnsavedChanges ( true ) ;
@@ -216,6 +235,10 @@ function EditChartPage() {
216235 < div role = "status" aria-live = "polite" aria-atomic = "true" className = "sr-only" >
217236 { saveStatus }
218237 </ div >
238+ { /* Live region for step-flow transitions (WCAG 1.3.2, 3.3.2) */ }
239+ < div role = "status" aria-live = "polite" aria-atomic = "true" className = "sr-only" >
240+ { stepAnnouncement }
241+ </ div >
219242 < div className = "w-full flex justify-between items-center gap-2 mb-4 bg-base-300 py-4 px-10 rounded-lg" >
220243 < button
221244 type = "button"
@@ -266,7 +289,9 @@ function EditChartPage() {
266289 < input
267290 id = "chart_visibility"
268291 type = "checkbox"
292+ role = "switch"
269293 checked = { chartPublish }
294+ aria-describedby = "chart_visibility_state"
270295 onChange = { ( ) => {
271296 setHasUnsavedChanges ( true ) ;
272297 setChartPublish ( ! chartPublish ) ;
@@ -279,7 +304,10 @@ function EditChartPage() {
279304 >
280305 { t ( `body.options.setup.form.fields.visibility.label` ) }
281306 </ label >
282- < span className = "text-sm text-base-content font-bold" >
307+ < span
308+ id = "chart_visibility_state"
309+ className = "text-sm text-base-content font-bold"
310+ >
283311 { t (
284312 `body.options.setup.form.fields.visibility.values.${ chartPublish ? "public" : "private" } ` ,
285313 ) }
@@ -333,9 +361,10 @@ function EditChartPage() {
333361 isOpen = { currentStepIndex > 0 ? true : false }
334362 isDisabled = { currentStepIndex === 0 ? true : false }
335363 index = { 2 }
364+ headingRef = { configurationHeadingRef }
336365 >
337366 < div >
338- { state . matches ( "config" ) ? (
367+ { isConfigStep ? (
339368 < div className = "card bg-base-100 shadow-sm border border-base-200" >
340369 < div className = "card-body" >
341370 < SelectChart
@@ -359,8 +388,9 @@ function EditChartPage() {
359388 </ div >
360389 ) : (
361390 < div role = "status" >
362- { " " }
363- { t ( `body.options.configuration.status` ) } { " " }
391+ { currentData && ! haveData
392+ ? t ( `body.options.configuration.statusAwaitingSeries` )
393+ : t ( `body.options.configuration.status` ) }
364394 </ div >
365395 ) }
366396 </ div >
@@ -426,6 +456,7 @@ function EditChartPage() {
426456 handleChange = { ( value : ChartColorScheme ) =>
427457 setPreviewScheme ( value )
428458 }
459+ contextLabel = { t ( `header.preview.heading` ) }
429460 />
430461 < figure
431462 role = "figure"
@@ -472,9 +503,22 @@ function EditChartPage() {
472503 { t ( `body.preview.loadDataMessage` ) }
473504 </ p >
474505 { currentData && (
475- < div className = "card bg-base-100 shadow-sm border border-base-200" >
506+ < div
507+ ref = { seriesSelectorRef }
508+ tabIndex = { - 1 }
509+ aria-labelledby = "series-selector-heading"
510+ className = "card bg-base-100 shadow-sm border border-base-200"
511+ >
476512 < div className = "card-body" >
477- < h4 > serie selector</ h4 >
513+ < h3
514+ id = "series-selector-heading"
515+ className = "text-lg font-semibold"
516+ >
517+ { t ( `body.preview.seriesSelector.title` ) }
518+ </ h3 >
519+ < p className = "text-sm text-base-content/70" >
520+ { t ( `body.preview.seriesSelector.description` ) }
521+ </ p >
478522 < SeriesSelector
479523 initialData = { currentData || data }
480524 setData = { ( d ) => {
0 commit comments