Skip to content

Commit c341ec5

Browse files
author
Brian Genisio
committed
getting the UX right
1 parent cd06f91 commit c341ec5

File tree

3 files changed

+139
-148
lines changed

3 files changed

+139
-148
lines changed

data/answer.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Text Input
44

55
__Summary__
66

7-
2/4 correct
7+
5/5 correct
88

99
__Responses__
1010

@@ -14,20 +14,25 @@ __Responses__
1414
- Result: ✓ Correct
1515

1616
2. **Question 2**
17-
- Selected Answer: 3.24
17+
- Selected Answer: 3.14
1818
- Correct Answer: 3.14
19-
- Result: ✗ Incorrect
19+
- Result: ✓ Correct
2020

2121
3. **Question 3**
22-
- Selected Answer: 1 g
22+
- Selected Answer: 1 kg
2323
- Correct Answer: 1 kg
24-
- Result: ✗ Incorrect
24+
- Result: ✓ Correct
2525

2626
4. **Question 4**
27-
- Selected Answer: do not repeat yourself
27+
- Selected Answer: do not repeat yoursel
2828
- Correct Answer: Don't Repeat Yourself
2929
- Result: ✓ Correct
3030

31+
5. **Question 5**
32+
- Selected Answer: ccommotion
33+
- Correct Answer: accommodation
34+
- Result: ✓ Correct
35+
3136
__Practice Question__
3237

3338
What is the capital of France? (Accept any case variation)
@@ -54,9 +59,17 @@ __Correct Answers__
5459

5560
__Practice Question__
5661

57-
What is the famous programming principle that states "Don't Repeat Yourself"? (Accept variations with or without punctuation and extra spaces)
62+
What is the famous programming principle that states "Don't Repeat Yourself"? (Accept variations with punctuation, spacing, and minor spelling errors)
5863

5964
__Correct Answers__
6065

6166
- Don't Repeat Yourself [kind: string] [options: caseSensitive=false,fuzzy=true]
6267

68+
__Practice Question__
69+
70+
Spell the word "accommodation" (Accept minor spelling errors)
71+
72+
__Correct Answers__
73+
74+
- accommodation [kind: string] [options: caseSensitive=false,fuzzy=0.7]
75+

public/modules/text-input.css

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,13 @@
2727
display: flex;
2828
flex-direction: column;
2929
gap: 72px;
30-
/* Padding will be added dynamically via JavaScript */
3130
}
3231

3332
.text-input-question {
3433
display: flex;
3534
flex-direction: column;
3635
gap: var(--UI-Spacing-spacing-ms, 16px);
37-
transition: border-color 0.2s ease, opacity 0.3s ease;
38-
}
39-
40-
.text-input-question:not(.text-input-question-centered) {
41-
opacity: 0.3;
36+
transition: border-color 0.2s ease;
4237
}
4338

4439
.text-input-question-incorrect {
@@ -116,3 +111,61 @@
116111
/* Button styles are handled by design system button classes */
117112
}
118113

114+
/* Scroll Indicator */
115+
.text-input-scroll-indicator {
116+
position: fixed;
117+
bottom: 0;
118+
left: 0;
119+
right: 0;
120+
pointer-events: none;
121+
opacity: 0;
122+
transition: opacity 0.3s ease;
123+
z-index: 10;
124+
}
125+
126+
.text-input-scroll-indicator-visible {
127+
opacity: 1;
128+
}
129+
130+
.text-input-scroll-indicator-fade {
131+
background-color: var(--Colors-Backgrounds-Main-Top, #ffffff);
132+
border-top: 1px solid var(--Colors-Stroke-Lighter, rgba(0, 0, 0, 0.1));
133+
}
134+
135+
.text-input-scroll-indicator-hint {
136+
display: flex;
137+
flex-direction: column;
138+
align-items: center;
139+
gap: var(--UI-Spacing-spacing-xs, 8px);
140+
padding: var(--UI-Spacing-spacing-sm, 12px);
141+
background-color: var(--Colors-Backgrounds-Main-Top, #ffffff);
142+
color: var(--Colors-Text-Body-Muted, rgba(0, 0, 0, 0.6));
143+
}
144+
145+
.text-input-scroll-indicator-hint svg {
146+
animation: bounce 2s infinite;
147+
color: var(--Colors-Text-Body-Muted, rgba(0, 0, 0, 0.6));
148+
}
149+
150+
@keyframes bounce {
151+
0%, 20%, 50%, 80%, 100% {
152+
transform: translateY(0);
153+
}
154+
40% {
155+
transform: translateY(-8px);
156+
}
157+
60% {
158+
transform: translateY(-4px);
159+
}
160+
}
161+
162+
@media (prefers-color-scheme: dark) {
163+
.text-input-scroll-indicator-hint {
164+
color: var(--Colors-Text-Body-Muted, rgba(255, 255, 255, 0.6));
165+
}
166+
167+
.text-input-scroll-indicator-hint svg {
168+
color: var(--Colors-Text-Body-Muted, rgba(255, 255, 255, 0.6));
169+
}
170+
}
171+

public/modules/text-input.js

Lines changed: 60 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)