@@ -15,11 +15,21 @@ export function initTextInput({ activity, state, postResults, persistedAnswers =
1515 elContainer . innerHTML = `
1616 <div id="text-input" class="text-input">
1717 <div id="text-input-questions" class="text-input-questions"></div>
18+ <div id="text-input-scroll-indicator" class="text-input-scroll-indicator" aria-hidden="true">
19+ <div class="text-input-scroll-indicator-fade"></div>
20+ <div class="text-input-scroll-indicator-hint">
21+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
22+ <path d="M7 10L12 15L17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
23+ </svg>
24+ <span class="body-xsmall">More questions below</span>
25+ </div>
26+ </div>
1827 </div>
1928 ` ;
2029
2130 const elTextInput = document . getElementById ( 'text-input' ) ;
2231 const elQuestions = document . getElementById ( 'text-input-questions' ) ;
32+ const elScrollIndicator = document . getElementById ( 'text-input-scroll-indicator' ) ;
2333
2434 // Track user answers per question
2535 const userAnswers = { } ;
@@ -265,10 +275,17 @@ export function initTextInput({ activity, state, postResults, persistedAnswers =
265275 nextButton . disabled = ! hasAnswer ;
266276 nextButton . setAttribute ( 'aria-label' , `Go to next question` ) ;
267277
268- // Scroll to next question when button is clicked
278+ // Next button click handler (no scrolling)
269279 nextButton . addEventListener ( 'click' , ( ) => {
270- const questionIndex = parseInt ( questionEl . getAttribute ( 'data-question-index' ) , 10 ) ;
271- scrollToNextQuestion ( questionIndex ) ;
280+ // Focus the next question's input field
281+ const nextQuestionIndex = qIdx + 1 ;
282+ if ( nextQuestionIndex < textInput . questions . length ) {
283+ const nextQuestion = textInput . questions [ nextQuestionIndex ] ;
284+ const nextInput = document . getElementById ( `q${ nextQuestion . id } -input` ) ;
285+ if ( nextInput ) {
286+ nextInput . focus ( ) ;
287+ }
288+ }
272289 } ) ;
273290
274291 // Update button state when input changes
@@ -284,102 +301,6 @@ export function initTextInput({ activity, state, postResults, persistedAnswers =
284301 elQuestions . appendChild ( questionEl ) ;
285302 } ) ;
286303
287- function findCenteredQuestionIndex ( ) {
288- const viewportCenter = window . innerHeight / 2 + window . scrollY ;
289- let closestQuestionIndex = 0 ;
290- let minDistance = Infinity ;
291-
292- for ( let i = 0 ; i < elQuestions . children . length ; i ++ ) {
293- const questionEl = elQuestions . children [ i ] ;
294- const rect = questionEl . getBoundingClientRect ( ) ;
295- const questionCenter = rect . top + rect . height / 2 + window . scrollY ;
296- const distance = Math . abs ( viewportCenter - questionCenter ) ;
297-
298- if ( distance < minDistance ) {
299- minDistance = distance ;
300- closestQuestionIndex = i ;
301- }
302- }
303-
304- return closestQuestionIndex ;
305- }
306-
307- function updateQuestionOpacity ( centeredQuestionIndex ) {
308- for ( let i = 0 ; i < elQuestions . children . length ; i ++ ) {
309- const questionEl = elQuestions . children [ i ] ;
310- if ( i === centeredQuestionIndex ) {
311- questionEl . classList . add ( 'text-input-question-centered' ) ;
312- } else {
313- questionEl . classList . remove ( 'text-input-question-centered' ) ;
314- }
315- }
316- }
317-
318- function updateDynamicPadding ( centeredQuestionIndex ) {
319- const viewportHeight = window . innerHeight ;
320-
321- // Calculate padding based on position
322- const totalQuestions = textInput . questions . length ;
323- const isFirstQuestion = centeredQuestionIndex === 0 ;
324- const isLastQuestion = centeredQuestionIndex === totalQuestions - 1 ;
325-
326- // Calculate how much padding is needed
327- let topPadding = 0 ;
328- let bottomPadding = 0 ;
329-
330- if ( isFirstQuestion ) {
331- // Need padding at top to center first question
332- const firstQuestionEl = elQuestions . children [ 0 ] ;
333- if ( firstQuestionEl ) {
334- const rect = firstQuestionEl . getBoundingClientRect ( ) ;
335- const questionHeight = rect . height ;
336- // Reduce padding slightly (multiply by 0.85) for better centering
337- const neededPadding = Math . max ( 0 , ( viewportHeight - questionHeight ) / 2 * 0.85 ) ;
338- topPadding = neededPadding ;
339- }
340- }
341-
342- if ( isLastQuestion ) {
343- // Need padding at bottom to center last question
344- const lastQuestionEl = elQuestions . children [ totalQuestions - 1 ] ;
345- if ( lastQuestionEl ) {
346- const rect = lastQuestionEl . getBoundingClientRect ( ) ;
347- const questionHeight = rect . height ;
348- const neededPadding = Math . max ( 0 , ( viewportHeight - questionHeight ) / 2 ) ;
349- bottomPadding = neededPadding ;
350- }
351- }
352-
353- // Apply padding dynamically
354- elQuestions . style . paddingTop = `${ topPadding } px` ;
355- elQuestions . style . paddingBottom = `${ bottomPadding } px` ;
356- }
357-
358- function centerQuestion ( questionIndex ) {
359- const questionEl = elQuestions . children [ questionIndex ] ;
360- if ( questionEl ) {
361- updateDynamicPadding ( questionIndex ) ;
362- updateQuestionOpacity ( questionIndex ) ;
363- questionEl . scrollIntoView ( {
364- behavior : 'smooth' ,
365- block : 'center' ,
366- inline : 'nearest'
367- } ) ;
368- // Update opacity and padding again after scroll animation completes
369- setTimeout ( ( ) => {
370- const centeredIndex = findCenteredQuestionIndex ( ) ;
371- updateQuestionOpacity ( centeredIndex ) ;
372- updateDynamicPadding ( centeredIndex ) ;
373- } , 600 ) ;
374- }
375- }
376-
377- function scrollToNextQuestion ( currentQuestionIndex ) {
378- const nextQuestionIndex = currentQuestionIndex + 1 ;
379- if ( nextQuestionIndex < textInput . questions . length ) {
380- centerQuestion ( nextQuestionIndex ) ;
381- }
382- }
383304
384305 function addErrorIcon ( questionEl ) {
385306 // Check if icon already exists
@@ -490,50 +411,54 @@ export function initTextInput({ activity, state, postResults, persistedAnswers =
490411 enabled : true
491412 } ) ;
492413
493- // Add scroll event listener to update opacity dynamically on manual scroll
494- let scrollTimeout ;
495- function handleScroll ( ) {
496- clearTimeout ( scrollTimeout ) ;
497- scrollTimeout = setTimeout ( ( ) => {
498- const centeredIndex = findCenteredQuestionIndex ( ) ;
499- updateQuestionOpacity ( centeredIndex ) ;
500- updateDynamicPadding ( centeredIndex ) ;
501- } , 50 ) ; // Debounce scroll events
502- }
503- window . addEventListener ( 'scroll' , handleScroll , { passive : true } ) ;
504- window . addEventListener ( 'resize' , handleScroll , { passive : true } ) ;
414+ // Add static top padding to position first question near the top
415+ elQuestions . style . paddingTop = '2rem' ;
505416
506- // Center the first question (or first answered question) on initial load
507- setTimeout ( ( ) => {
508- let questionToCenter = 0 ; // Default to first question
417+ // Function to check if there's content below the fold and update scroll indicator
418+ function updateScrollIndicator ( ) {
419+ if ( ! elScrollIndicator ) return ;
420+
421+ const containerRect = elTextInput . getBoundingClientRect ( ) ;
422+ const questionsRect = elQuestions . getBoundingClientRect ( ) ;
423+ const viewportHeight = window . innerHeight ;
424+
425+ // Check if questions container extends below the visible viewport
426+ const isContentBelow = questionsRect . bottom > viewportHeight ;
509427
510- // If persisted answers exist, always scroll to the first question
511- if ( persistedAnswers ) {
512- questionToCenter = 0 ;
428+ // Also check if user has scrolled near the bottom (within 100px)
429+ const scrollPosition = window . scrollY + viewportHeight ;
430+ const documentHeight = document . documentElement . scrollHeight ;
431+ const isNearBottom = scrollPosition >= documentHeight - 100 ;
432+
433+ if ( isContentBelow && ! isNearBottom ) {
434+ elScrollIndicator . classList . add ( 'text-input-scroll-indicator-visible' ) ;
513435 } else {
514- // Otherwise, check if there's a pre-answered question from state
515- for ( let i = 0 ; i < textInput . questions . length ; i ++ ) {
516- const q = textInput . questions [ i ] ;
517- if ( userAnswers [ q . id ] && userAnswers [ q . id ] . trim ( ) . length > 0 ) {
518- questionToCenter = i ;
519- break ;
520- }
521- }
436+ elScrollIndicator . classList . remove ( 'text-input-scroll-indicator-visible' ) ;
522437 }
523-
524- centerQuestion ( questionToCenter ) ;
525- // Update opacity and padding after scroll animation completes
526- setTimeout ( ( ) => {
527- const centeredIndex = findCenteredQuestionIndex ( ) ;
528- updateQuestionOpacity ( centeredIndex ) ;
529- updateDynamicPadding ( centeredIndex ) ;
530- } , 600 ) ; // Wait for smooth scroll to complete
531- } , 100 ) ;
438+ }
439+
440+ // Update scroll indicator on scroll and resize
441+ let scrollIndicatorTimeout ;
442+ function handleScrollIndicatorUpdate ( ) {
443+ clearTimeout ( scrollIndicatorTimeout ) ;
444+ scrollIndicatorTimeout = setTimeout ( ( ) => {
445+ updateScrollIndicator ( ) ;
446+ } , 100 ) ;
447+ }
448+
449+ window . addEventListener ( 'scroll' , handleScrollIndicatorUpdate , { passive : true } ) ;
450+ window . addEventListener ( 'resize' , handleScrollIndicatorUpdate , { passive : true } ) ;
451+
452+ // Initial check after questions are rendered
453+ setTimeout ( ( ) => {
454+ updateScrollIndicator ( ) ;
455+ } , 200 ) ;
532456
533457 return {
534458 cleanup : ( ) => {
535459 toolbar . unregisterTool ( 'text-input-clear-all' ) ;
536- window . removeEventListener ( 'scroll' , handleScroll ) ;
460+ window . removeEventListener ( 'scroll' , handleScrollIndicatorUpdate ) ;
461+ window . removeEventListener ( 'resize' , handleScrollIndicatorUpdate ) ;
537462 elContainer . innerHTML = '' ;
538463 } ,
539464 validate : validateAnswers
0 commit comments