@@ -11,7 +11,7 @@ import {
1111 useSearchCounts
1212} from '@Services/search' ;
1313import { useTranscripts } from '@Services/ramp-hooks' ;
14- import { autoScroll , screenReaderFriendlyTime , timeToHHmmss } from '@Services/utility-helpers' ;
14+ import { autoScroll , screenReaderFriendlyText , screenReaderFriendlyTime , timeToHHmmss } from '@Services/utility-helpers' ;
1515import Spinner from '@Components/Spinner' ;
1616import './Transcript.scss' ;
1717
@@ -114,6 +114,20 @@ const TranscriptLine = memo(({
114114 const onClick = ( e ) => {
115115 e . preventDefault ( ) ;
116116 e . stopPropagation ( ) ;
117+
118+ // Handle click on a link in the cue text in the same tab
119+ if ( e . target . tagName == 'A' ) {
120+ // Check if the href value is a valid URL before navigation
121+ const urlRegex = / h t t p s ? : \/ \/ [ ^ \s / $ . ? # ] .[ ^ \s ] * / gi;
122+ const href = e . target . getAttribute ( 'href' ) ;
123+ if ( ! href ?. match ( urlRegex ) ) {
124+ e . preventDefault ( ) ;
125+ } else {
126+ window . open ( href , '_self' ) ;
127+ return ;
128+ }
129+ }
130+
117131 if ( item . match && focusedMatchId !== item . id ) {
118132 setFocusedMatchId ( item . id ) ;
119133 } else if ( focusedMatchId !== null && item . tag === TRANSCRIPT_CUE_TYPES . timedCue ) {
@@ -138,7 +152,7 @@ const TranscriptLine = memo(({
138152
139153 const cueText = useMemo ( ( ) => {
140154 return buildSpeakerText ( item , item . tag === TRANSCRIPT_CUE_TYPES . nonTimedLine ) ;
141- } , [ item ?. tag ] ) ;
155+ } , [ item ] ) ;
142156
143157 /** Build text portion of the transcript cue element */
144158 const cueTextElement = useMemo ( ( ) => {
@@ -171,22 +185,30 @@ const TranscriptLine = memo(({
171185
172186 return (
173187 < span
174- role = "radio"
175- tabIndex = { isFirstItem ? 0 : - 1 }
176- aria-checked = { isActive }
177188 ref = { itemRef }
178- onClick = { onClick }
179- onKeyDown = { handleKeyDown }
180189 className = { cx (
181190 'ramp--transcript_item' ,
182191 isActive && 'active' ,
183- isFocused && 'focused'
192+ isFocused && 'focused' ,
193+ item . tag != TRANSCRIPT_CUE_TYPES . timedCue && 'untimed' ,
184194 ) }
185195 data-testid = { testId }
186- aria-label = { `${ screenReaderFriendlyTime ( item . begin ) } , ${ cueText } ` }
196+ /* For untimed cues,
197+ - set tabIndex for keyboard navigation
198+ - onClick handler to scroll them to top on click
199+ - set aria-label with full cue text */
200+ tabIndex = { isFirstItem && item . begin == undefined ? 0 : - 1 }
201+ onClick = { item . begin == undefined ? onClick : null }
202+ aria-label = { item . begin == undefined && screenReaderFriendlyText ( cueText ) }
187203 >
188204 { item . tag === TRANSCRIPT_CUE_TYPES . timedCue && typeof item . begin === 'number' && (
189- < span className = "ramp--transcript_time" data-testid = "transcript_time" >
205+ < span className = 'ramp--transcript_time' data-testid = 'transcript_time'
206+ role = 'button'
207+ onClick = { onClick }
208+ onKeyDown = { handleKeyDown }
209+ tabIndex = { isFirstItem ? 0 : - 1 }
210+ aria-label = { `${ screenReaderFriendlyTime ( item . begin ) } , ${ screenReaderFriendlyText ( cueText ) } ` }
211+ >
190212 [{ timeToHHmmss ( item . begin , true ) } ]
191213 </ span >
192214 ) }
@@ -247,19 +269,24 @@ const TranscriptList = memo(({
247269 * @param {Event } e keyboard event
248270 */
249271 const handleKeyDown = ( e ) => {
250- const cues = transcriptListRef . current . children ;
251- if ( cues ?. length > 0 ) {
272+ // Get the timestamp for each cue for timed transcript, as these are focusable
273+ const cueTimes = transcriptListRef . current . querySelectorAll ( '.ramp--transcript_time' ) ;
274+ // Get the non-empty cues for untimed transcript
275+ const cueList = Array . from ( transcriptListRef . current . children ) . filter ( ( c ) => c . textContent ?. length > 0 ) ;
276+
277+ const cueLength = cueTimes ?. length || cueList ?. length || 0 ;
278+ if ( cueLength > 0 ) {
252279 let nextIndex = currentIndex . current ;
253280 /**
254281 * Default behavior is prevented (e.preventDefault()) only for the handled
255282 * key combinations to allow other keyboard shortcuts to work as expected.
256283 */
257284 if ( e . key === 'ArrowDown' ) {
258285 // Wraps focus back to first cue when the end of transcript is reached
259- nextIndex = ( currentIndex . current + 1 ) % cues . length ;
286+ nextIndex = ( currentIndex . current + 1 ) % cueLength ;
260287 e . preventDefault ( ) ;
261288 } else if ( e . key === 'ArrowUp' ) {
262- nextIndex = ( currentIndex . current - 1 + cues . length ) % cues . length ;
289+ nextIndex = ( currentIndex . current - 1 + cueLength ) % cueLength ;
263290 e . preventDefault ( ) ;
264291 } else if ( e . key === 'Tab' && e . shiftKey ) {
265292 // Returns focus to parent container on (Shift + Tab) key combination press
@@ -268,11 +295,21 @@ const TranscriptList = memo(({
268295 return ;
269296 }
270297 if ( nextIndex !== currentIndex . current ) {
271- cues [ currentIndex . current ] . tabIndex = - 1 ;
272- cues [ nextIndex ] . tabIndex = 0 ;
273- cues [ nextIndex ] . focus ( ) ;
274- // Scroll the cue into view
275- autoScroll ( cues [ nextIndex ] , transcriptContainerRef ) ;
298+ if ( cueTimes ?. length > 0 ) {
299+ // Use timestamps of timed cues for navigation
300+ cueTimes [ currentIndex . current ] . tabIndex = - 1 ;
301+ cueTimes [ nextIndex ] . tabIndex = 0 ;
302+ cueTimes [ nextIndex ] . focus ( ) ;
303+ // Scroll the cue into view
304+ autoScroll ( cueTimes [ nextIndex ] , transcriptContainerRef ) ;
305+ } else if ( cueList ?. length > 0 ) {
306+ // Use whole cues for navigation for untimed cues
307+ cueList [ currentIndex . current ] . tabIndex = - 1 ;
308+ cueList [ nextIndex ] . tabIndex = 0 ;
309+ cueList [ nextIndex ] . focus ( ) ;
310+ // Scroll the cue to the top of container
311+ autoScroll ( cueList [ nextIndex ] , transcriptContainerRef , true ) ;
312+ }
276313 setCurrentIndex ( nextIndex ) ;
277314 }
278315 }
@@ -292,7 +329,6 @@ const TranscriptList = memo(({
292329 return (
293330 < div
294331 data-testid = { `transcript_${ testId } ` }
295- role = "radiogroup"
296332 onKeyDown = { handleKeyDown }
297333 ref = { transcriptListRef }
298334 aria-label = 'Scrollable transcript cues'
0 commit comments