Skip to content

auto-advance, delay and block answer change - #1449

Open
A7700 wants to merge 7 commits into
devfrom
al/1447_autoAdvance_allowResponseChange
Open

A7700 wants to merge 7 commits into
devfrom
al/1447_autoAdvance_allowResponseChange

Conversation

@A7700

@A7700 A7700 commented Aug 31, 2026

Copy link
Copy Markdown

Does this PR close any open issues?

Closes #1447

Give a longer description of what this PR addresses and why it's needed

This PR adds the options autoAdvanceToNextStep, autoAdvanceDelay and allowResponseChange to the ButtonsResponse. Studies sometimes want the user to automatically navigate to the next page/question if some input was given especially when a button is pressed. The options introduced allow to automatically proceed to the next page. This procedure can be delayed and the given answer can be locked so it cannot be changed even if the delay is active and the next question is not shown immediately.

@A7700
A7700 marked this pull request as draft August 31, 2026 13:42
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

A preview of ee4ecc9 is uploaded and can be seen here:

https://revisit.dev/study/PR1449

Changes may take a few minutes to propagate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ec7cf9b31

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +692 to +694
autoAdvanceTimeoutRef.current = setTimeout(() => {
handleNextClick();
}, readyResponse.autoAdvanceDelay ?? 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist the latest selection when the delay expires

With autoAdvanceDelay and the default allowResponseChange: true, selecting A and then B before the timeout expires displays B but can persist A. The timeout closes over the handleNextClick/goToNextStep instance—and therefore trialValidation—from the render that armed it, while the identifier latch prevents later renders from replacing the callback. Invoke the latest callback through a ref or cancel and re-arm the timer when the answer changes.

Useful? React with 👍 / 👎.

autoAdvanceIdentifierRef.current = identifier;
if (readyResponse.type === 'buttons') {
autoAdvanceTimeoutRef.current = setTimeout(() => {
handleNextClick();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep auto-advance behind the training gate

For a buttons component with provideFeedback and correctAnswer, the normal Next button is disabled until Check Answer records a correct response or exhausts the configured attempts, but this direct call advances after any ordinarily valid selection. An incorrect answer therefore skips feedback and attempt tracking entirely; disable auto-advance for feedback components or route it through the same Check Answer state machine.

Useful? React with 👍 / 👎.

return;
}

const readyResponse = autoAdvanceResponses.find((response) => hasAnswerValue(combinedValues[response.id]));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trigger auto-advance only from a new selection

This value-based condition also matches answers that were not entered during the current visit. A configured buttons default skips the step without participant interaction, and a persisted answer restored after the participant clicks Previous immediately sends them forward again, preventing review or editing even when backward navigation is enabled. Arm auto-advance only on a new participant selection rather than on any nonempty initialized value.

Useful? React with 👍 / 👎.

Comment thread src/parser/parser.ts Outdated
Comment on lines +777 to +778
if (!component.response || !Array.isArray(component.response)) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate inherited responses before issuing warnings

When a component declares only baseComponent, this early return skips the warning even if the resolved base component contains an auto-advance button plus another required response. Runtime rendering merges that base through studyComponentToIndividualComponent, so the unsafe multi-response configuration is accepted without the warning introduced here. Run this check against the resolved component instead of the raw override.

Useful? React with 👍 / 👎.

@A7700
A7700 marked this pull request as ready for review September 1, 2026 13:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66193a3f41

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +691 to +695
const prevValuesRef = useRef(answerValidator.values);
useEffect(() => {
if (!isEqual(prevValuesRef.current, answerValidator.values)) {
if (Object.keys(answerValidator.values).length > 0) {
userInteractedRef.current = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track interactions from the auto-advance response block

When an auto-advance button is in sidebar or aboveStimulus while nextButtonLocation is elsewhere, auto-advance never fires. Only the ResponseBlock containing the Next button becomes autoAdvanceOwner, but this interaction flag watches that block's location-filtered answerValidator.values; the block containing the clicked button sees the interaction but is not the owner, while the owner sees the shared answer without setting this flag. Track the auto-advance response's transition in combinedValues or propagate its input event to the owner.

Useful? React with 👍 / 👎.

Comment on lines +109 to +110
if (response.allowResponseChange === false) {
return hasAnswerValue(ans.value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not lock a configured default before interaction

For a buttons response combining default with allowResponseChange: false, generateInitFields initializes ans.value from the default, so this condition immediately disables every option before the participant has selected anything. The participant can therefore never replace the preselected default; base the lock on the first actual response interaction rather than merely on whether the initialized field has a value.

Useful? React with 👍 / 👎.

@A7700 A7700 linked an issue Sep 2, 2026 that may be closed by this pull request
@JackWilb

JackWilb commented Sep 3, 2026

Copy link
Copy Markdown
Contributor
  • Demo form elements
  • Remove next by default on auto advance
  • Add flag for removing the next button

@A7700
A7700 force-pushed the al/1447_autoAdvance_allowResponseChange branch from 5ac01f0 to ca179dc Compare September 7, 2026 12:38

@JackWilb JackWilb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. The main flow needs a few correctness fixes before merge. The inline comments call out the issues affecting training feedback, restored/default answers, response locking, inherited components, navigation timers, and test coverage. Please address those cases and add focused component-level tests for the real user interactions.

return undefined;
}

const hasFeedback = 'provideFeedback' in readyResponse && Boolean(readyResponse.provideFeedback);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check looks for provideFeedback on the response, but training feedback is configured on the component. For a valid training component this is always false, so the timer can advance without Check Answer and skip incorrect-answer attempts. Please gate this path with the same training state used by the Next button.

handleNextClickRef.current = handleNextClick;
}, [handleNextClick]);

const prevCombinedValuesRef = useRef(combinedValues);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing the shared Redux values treats initialization as a participant interaction. A default or restored answer can therefore arm this timer without a new button selection, including when the participant returns with Previous. Please pass an explicit selection event from the buttons response and arm the timer only for that event.

return false;
}

if (response.allowResponseChange === false) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing a default sends an empty value through the same interaction path, sets the interaction flag, and then locks every option when allowResponseChange is false. That leaves a required response empty and uneditable. Only lock after a non-empty option is selected, or make Clear Selection unavailable in this mode.

Comment thread src/parser/parser.ts
(response) => response.type === 'buttons' && response.autoAdvanceToNextStep === true,
);
const isAutoAdvance = hasAutoAdvanceButton || resolvedComponent.nextButtonAutoAdvanceTime !== undefined;
resolvedComponent.nextButtonHidden = resolvedComponent.nextButtonHidden ?? (isAutoAdvance ? true : ((studyConfig.uiConfig as { nextButtonHidden?: boolean })?.nextButtonHidden ?? false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an inherited component, studyComponentToIndividualComponent returns a merged clone, so this nextButtonHidden default is discarded. Runtime resolution then falls back to showing Next, contrary to the auto-advance default. Compute the default at the runtime owner or persist it on the actual component entry, and cover inherited/library cases.

autoAdvanceIdentifierRef.current = identifier;
if (readyResponse.type === 'buttons') {
const delay = readyResponse.autoAdvanceDelay ?? 0;
autoAdvanceTimeoutRef.current = setTimeout(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This direct call bypasses NextButton's nextButtonEnableTime and nextButtonDisableTime checks. A response can advance before Next is enabled or after it has timed out. Please share the navigation eligibility check with response auto-advance, or reject/document incompatible timer combinations.

expect(mockNavigate).toHaveBeenCalledWith('/study-1/1');
});

test('locks response options immediately during autoAdvanceDelay before navigation occurs', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests do not mount ResponseBlock or ResponseSwitcher; they manually invoke goToNextStep and create unrelated timers. They would pass if the new feature were removed. Please add focused tests that render the response UI and exercise real selection, delay, training, Previous/default, cross-location, and clear-selection behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-advance on submitting a response in forms

2 participants