Skip to content

Commit 3a3fd5a

Browse files
jodeleeuwclaude
andcommitted
docs: clarify wait_for_key_release tracks only the most recent key press
With persist: true, a listener holds one pending release at a time; a later valid press supersedes an unreleased earlier one. Document this intended behavior in the API reference and a code comment, and add a test that locks it in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 76c557e commit 3a3fd5a

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

docs/reference/jspsych-pluginAPI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ audio_context_start_time | numeric | The scheduled time of the sound file in the
165165
allow_held_key | boolean | If `true`, then responses will be registered from keys that are being held down. If `false`, then a held key can only register a response the first time that `getKeyboardResponse` is called for that key. For example, if a participant holds down the `A` key before the experiment starts, then the first time `getKeyboardResponse` is called, the `A` will register as a key press. However, any future calls to `getKeyboardResponse` will not register the `A` until the participant releases the key and presses it again.
166166
persist | boolean | If false, then the keyboard listener will only trigger the first time a valid key is pressed. If true, then it will trigger every time a valid key is pressed until it is explicitly cancelled by `jsPsych.pluginAPI.cancelKeyboardResponse` or `jsPsych.pluginAPI.cancelAllKeyboardResponses`.
167167
minimum_valid_rt | number | The minimum valid response time for key presses. Any key press response time that is less than this value will be treated as invalid and ignored.
168-
wait_for_key_release | boolean | If `true`, the `callback_function` is not triggered when the key is pressed down. Instead, it is triggered when the response key is released (on `keyup`), and the object passed to it gains a third property, `rt_key_duration`, in addition to `key` and `rt`. `rt` still reflects the time of the key press, while `rt_key_duration` is the time in milliseconds that the key was held down, measured from the initial key press to the key release (or `null` if no press timestamp was recorded). If the keyboard listener is cancelled by `jsPsych.pluginAPI.cancelKeyboardResponse` or `jsPsych.pluginAPI.cancelAllKeyboardResponses` before the key is released, the deferred callback will not fire. `rt_key_duration` is always measured with `performance.now()`, regardless of `rt_method`. Defaults to `false`, in which case the callback fires at the key press with only `key` and `rt`.
168+
wait_for_key_release | boolean | If `true`, the `callback_function` is not triggered when the key is pressed down. Instead, it is triggered when the response key is released (on `keyup`), and the object passed to it gains a third property, `rt_key_duration`, in addition to `key` and `rt`. `rt` still reflects the time of the key press, while `rt_key_duration` is the time in milliseconds that the key was held down, measured from the initial key press to the key release (or `null` if no press timestamp was recorded). If the keyboard listener is cancelled by `jsPsych.pluginAPI.cancelKeyboardResponse` or `jsPsych.pluginAPI.cancelAllKeyboardResponses` before the key is released, the deferred callback will not fire. `rt_key_duration` is always measured with `performance.now()`, regardless of `rt_method`. When combined with `persist: true`, a listener tracks only one pending key release at a time: the release being awaited is always that of the most recent valid key press. If a second valid key is pressed before the first is released, the first press is superseded and its release will not fire the callback (only the most recent press's release is reported). Defaults to `false`, in which case the callback fires at the key press with only `key` and `rt`.
169169

170170
#### Return value
171171

packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ export class KeyboardListenerAPI {
158158
// defer the callback until this key is released, keyed by the listener handle so that
159159
// the pending release is cancelled together with the listener (see cancelKeyboardResponse
160160
// and cancelAllKeyboardResponses).
161+
//
162+
// Because the pending release is keyed by listener, each listener tracks only one pending
163+
// release at a time. With persist: true, if a second valid key is pressed before the first
164+
// is released, this overwrites the earlier pending release: only the most recent press's
165+
// release fires the callback. This is intentional — a deferred response reflects the most
166+
// recent key press, matching how the non-deferred path only reports the latest press.
161167
this.pendingReleases.set(listener, { key, rt, callback_function });
162168
} else {
163169
callback_function({ key: e.key, rt });

packages/jspsych/tests/pluginAPI/pluginapi.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,31 @@ describe("#getKeyboardResponse wait_for_key_release", () => {
349349
expect.objectContaining({ key: "a", rt_key_duration: 250 })
350350
);
351351
});
352+
353+
test("persist: true: a second press before the first is released supersedes the first; only the latest press's release fires", async () => {
354+
new KeyboardListenerAPI(getRootElement).getKeyboardResponse({
355+
callback_function: callback,
356+
wait_for_key_release: true,
357+
persist: true,
358+
});
359+
360+
// press "a", then press "b" before releasing "a"
361+
await keyDown("a");
362+
jest.advanceTimersByTime(100);
363+
await keyDown("b");
364+
jest.advanceTimersByTime(100);
365+
366+
// releasing "a" does not fire: it was superseded by the "b" press
367+
await keyUp("a");
368+
expect(callback).toHaveBeenCalledTimes(0);
369+
370+
// releasing "b" fires with the duration measured from the "b" press
371+
await keyUp("b");
372+
expect(callback).toHaveBeenCalledTimes(1);
373+
expect(callback).toHaveBeenLastCalledWith(
374+
expect.objectContaining({ key: "b", rt_key_duration: 100 })
375+
);
376+
});
352377
});
353378

354379
describe("#cancelKeyboardResponse", () => {

0 commit comments

Comments
 (0)