Skip to content

Commit 7f18b80

Browse files
authored
Merge pull request #1305 from revisit-studies/codex/issue-1284-trrack-replay
Fix Trrack undo/redo replay
2 parents 809b424 + 3313498 commit 7f18b80

24 files changed

Lines changed: 831 additions & 172 deletions

public/demo-html-trrack/assets/dots-count.html

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,40 @@
55
<meta charset="UTF-8">
66
<title>Trrack Demo</title>
77
<script src="https://d3js.org/d3.v7.js"></script>
8+
<style>
9+
.limit-control {
10+
display: inline-block;
11+
position: relative;
12+
}
13+
14+
.limit-tooltip {
15+
background: #1f2933;
16+
border-radius: 4px;
17+
bottom: calc(100% + 8px);
18+
color: #ffffff;
19+
font: 12px/1.4 Arial, Helvetica, sans-serif;
20+
left: 50%;
21+
opacity: 0;
22+
padding: 6px 8px;
23+
pointer-events: none;
24+
position: absolute;
25+
transform: translateX(-50%);
26+
visibility: hidden;
27+
white-space: nowrap;
28+
z-index: 1;
29+
}
830

31+
.limit-control:hover .limit-tooltip,
32+
.limit-control:focus .limit-tooltip,
33+
.limit-control:focus-within .limit-tooltip {
34+
opacity: 1;
35+
visibility: visible;
36+
}
37+
38+
.limit-tooltip[hidden] {
39+
display: none;
40+
}
41+
</style>
942
</head>
1043

1144
<body>
@@ -17,8 +50,14 @@
1750
<div class="non-btns">
1851
<div id="dots">
1952
<div>
20-
<button id="add">Add</button>
21-
<button id="rem">Remove</button>
53+
<span class="limit-control" id="add-control">
54+
<button id="add">Add</button>
55+
<span class="limit-tooltip" id="add-limit-tooltip" role="tooltip" hidden>Maximum of 20 dots reached.</span>
56+
</span>
57+
<span class="limit-control" id="remove-control">
58+
<button id="rem">Remove</button>
59+
<span class="limit-tooltip" id="remove-limit-tooltip" role="tooltip" hidden>Minimum of 0 dots reached.</span>
60+
</span>
2261
</div>
2362
</div>
2463
</div>
@@ -32,6 +71,7 @@
3271

3372
// Task id should match the task's answer id in the config file
3473
const taskId = 'circlesCount';
74+
const maxDots = 20;
3575

3676
function dots(initialDots) {
3777
const height = 50;
@@ -110,31 +150,69 @@
110150
// Render the dots on screen using state from trrack
111151
const dotActions = dots(trrack.getState().dots);
112152

113-
// Register a listener to update the dot chart based on trrack updates.
114-
trrack.currentChange(() => {
153+
const addButton = document.getElementById("add");
154+
const removeButton = document.getElementById("rem");
155+
const addControl = document.getElementById("add-control");
156+
const removeControl = document.getElementById("remove-control");
157+
const addTooltip = document.getElementById("add-limit-tooltip");
158+
const removeTooltip = document.getElementById("remove-limit-tooltip");
159+
160+
function setLimitState(button, control, tooltip, disabled) {
161+
button.disabled = disabled;
162+
tooltip.hidden = !disabled;
163+
164+
if (disabled) {
165+
button.setAttribute("aria-describedby", tooltip.id);
166+
control.tabIndex = 0;
167+
} else {
168+
button.removeAttribute("aria-describedby");
169+
control.removeAttribute("tabindex");
170+
}
171+
}
172+
173+
function renderState(currentDots) {
174+
dotActions.update(currentDots);
175+
setLimitState(addButton, addControl, addTooltip, currentDots.length >= maxDots);
176+
setLimitState(removeButton, removeControl, removeTooltip, currentDots.length === 0);
177+
}
178+
179+
function publishState() {
115180
const dots = trrack.getState().dots;
116-
dotActions.update(dots);
181+
renderState(dots);
117182
Revisit.postAnswers({
118183
[taskId]: dots.length
119184
});
120185
Revisit.postProvenance(trrack.graph.backend);
121-
});
186+
}
187+
188+
// Register a listener to update the dot chart based on trrack updates.
189+
trrack.currentChange(publishState);
122190

123191
// Bind button to update dots
124-
document.getElementById("add").onclick = () =>
125-
trrack.apply("Add Dot", updateDots("add"));
192+
addButton.onclick = () => {
193+
if (trrack.getState().dots.length < maxDots) {
194+
trrack.apply("Add Dot", updateDots("add"));
195+
}
196+
};
126197

127-
document.getElementById("rem").onclick = () =>
128-
trrack.apply("Remove Dot", updateDots("remove"));
198+
removeButton.onclick = () => {
199+
if (trrack.getState().dots.length > 0) {
200+
trrack.apply("Remove Dot", updateDots("remove"));
201+
}
202+
};
129203

130204
// Bind undo/redo
131205
document.getElementById("undo").onclick = () => trrack.undo();
132206
document.getElementById("redo").onclick = () => trrack.redo();
133207

134208
Revisit.onProvenanceReceive((prov) => {
135-
dotActions.update(prov.dots);
136-
})
209+
if (Array.isArray(prov?.dots)) {
210+
renderState(prov.dots);
211+
}
212+
});
213+
214+
publishState();
137215

138216
</script>
139217

140-
</html>
218+
</html>

public/demo-svelte-trrack/assets/DotCounter.svelte

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
export let redo;
77
88
$: visibleDots = Array.isArray(dots) ? dots : [];
9+
$: atMaximum = visibleDots.length >= 20;
10+
$: atMinimum = visibleDots.length === 0;
911
</script>
1012

1113
<div class="container">
@@ -15,8 +17,18 @@
1517
</div>
1618

1719
<div class="controls">
18-
<button type="button" on:click={addDot}>Add</button>
19-
<button type="button" on:click={removeDot}>Remove</button>
20+
<span class="limit-control" tabindex={atMaximum ? 0 : undefined}>
21+
<button type="button" on:click={addDot} disabled={atMaximum} aria-describedby={atMaximum ? 'add-limit-tooltip' : undefined}>Add</button>
22+
{#if atMaximum}
23+
<span class="limit-tooltip" id="add-limit-tooltip" role="tooltip">Maximum of 20 dots reached.</span>
24+
{/if}
25+
</span>
26+
<span class="limit-control" tabindex={atMinimum ? 0 : undefined}>
27+
<button type="button" on:click={removeDot} disabled={atMinimum} aria-describedby={atMinimum ? 'remove-limit-tooltip' : undefined}>Remove</button>
28+
{#if atMinimum}
29+
<span class="limit-tooltip" id="remove-limit-tooltip" role="tooltip">Minimum of 0 dots reached.</span>
30+
{/if}
31+
</span>
2032
</div>
2133

2234
<svg viewBox="0 0 500 50" width="500" height="50" aria-label="Dot count">
@@ -30,3 +42,34 @@
3042
{/each}
3143
</svg>
3244
</div>
45+
46+
<style>
47+
.limit-control {
48+
display: inline-block;
49+
position: relative;
50+
}
51+
52+
.limit-tooltip {
53+
background: #1f2933;
54+
border-radius: 4px;
55+
bottom: calc(100% + 8px);
56+
color: #ffffff;
57+
font: 12px/1.4 Arial, Helvetica, sans-serif;
58+
left: 50%;
59+
opacity: 0;
60+
padding: 6px 8px;
61+
pointer-events: none;
62+
position: absolute;
63+
transform: translateX(-50%);
64+
visibility: hidden;
65+
white-space: nowrap;
66+
z-index: 1;
67+
}
68+
69+
.limit-control:hover .limit-tooltip,
70+
.limit-control:focus .limit-tooltip,
71+
.limit-control:focus-within .limit-tooltip {
72+
opacity: 1;
73+
visibility: visible;
74+
}
75+
</style>

public/demo-svelte-trrack/assets/dots-count.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import { Registry, initializeTrrack } from 'https://cdn.jsdelivr.net/npm/@trrack/core@1.3.0/+esm';
6060

6161
const taskId = 'circlesCount';
62+
const maxDots = 20;
6263
const initialState = {
6364
dots: [1]
6465
};
@@ -97,12 +98,25 @@
9798

9899
try {
99100
const DotCounter = await loadComponent();
101+
102+
function addDot() {
103+
if (trrack.getState().dots.length < maxDots) {
104+
trrack.apply('Add Dot', updateDots('add'));
105+
}
106+
}
107+
108+
function removeDot() {
109+
if (trrack.getState().dots.length > 0) {
110+
trrack.apply('Remove Dot', updateDots('remove'));
111+
}
112+
}
113+
100114
const component = new DotCounter({
101115
target: document.getElementById('app'),
102116
props: {
103117
dots: trrack.getState().dots,
104-
addDot: () => trrack.apply('Add Dot', updateDots('add')),
105-
removeDot: () => trrack.apply('Remove Dot', updateDots('remove')),
118+
addDot,
119+
removeDot,
106120
undo: () => trrack.undo(),
107121
redo: () => trrack.redo()
108122
}

src/analysis/individualStudy/stats/tests/StatsView.spec.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from 'react';
1+
import { forwardRef, ReactNode } from 'react';
22
import { renderToStaticMarkup } from 'react-dom/server';
33
import {
44
beforeEach, describe, expect, test, vi,
@@ -31,7 +31,9 @@ vi.mock('@mantine/core', () => ({
3131
Box: ({ children }: { children: ReactNode }) => <div>{children}</div>,
3232
Divider: () => <hr />,
3333
Flex: ({ children }: { children: ReactNode }) => <div>{children}</div>,
34-
Paper: ({ children, ref: _ref }: { children: ReactNode; ref?: React.Ref<HTMLElement> }) => <div>{children}</div>,
34+
Paper: forwardRef<HTMLDivElement, { children: ReactNode }>(function Paper({ children }, ref) { // eslint-disable-line prefer-arrow-callback
35+
return <div ref={ref}>{children}</div>;
36+
}),
3537
Text: ({ children }: { children: ReactNode }) => <p>{children}</p>,
3638
Title: ({ children }: { children: ReactNode }) => <h5>{children}</h5>,
3739
Collapse: ({ children, in: open }: { children: ReactNode; in?: boolean }) => (

src/analysis/individualStudy/thinkAloud/tests/ThinkAloudAnalysis.spec.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from 'react';
1+
import { forwardRef, ReactNode } from 'react';
22
import { renderToStaticMarkup } from 'react-dom/server';
33
import {
44
render, act, cleanup, fireEvent, waitFor,
@@ -116,7 +116,11 @@ vi.mock('@mantine/core', () => ({
116116
),
117117
Grid: Object.assign(
118118
({ children }: { children: ReactNode }) => <div>{children}</div>,
119-
{ Col: ({ children, ref: _r, ...rest }: { children: ReactNode; ref?: React.Ref<HTMLElement> }) => <div {...rest}>{children}</div> },
119+
{
120+
Col: forwardRef<HTMLDivElement, { children: ReactNode }>(function Col({ children, ...rest }, ref) { // eslint-disable-line prefer-arrow-callback
121+
return <div ref={ref} {...rest}>{children}</div>;
122+
}),
123+
},
120124
),
121125
Group: ({ children, style }: { children: ReactNode; style?: object }) => <div style={style}>{children}</div>,
122126
HoverCard: Object.assign(
@@ -157,17 +161,19 @@ vi.mock('@mantine/core', () => ({
157161
{rightSection}
158162
</div>
159163
),
160-
Stack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
164+
Stack: forwardRef<HTMLDivElement, { children: ReactNode }>(function Stack({ children }, ref) { // eslint-disable-line prefer-arrow-callback
165+
return <div ref={ref}>{children}</div>;
166+
}),
161167
Text: ({ children }: { children: ReactNode }) => <p>{children}</p>,
162-
Textarea: ({
163-
value, onChange, onKeyDown, onFocus, placeholder, onBlur, defaultValue,
164-
}: {
168+
Textarea: forwardRef<HTMLTextAreaElement, {
165169
value?: string; defaultValue?: string; onChange?: React.ChangeEventHandler<HTMLTextAreaElement>;
166170
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>; onFocus?: React.FocusEventHandler<HTMLTextAreaElement>;
167171
placeholder?: string; onBlur?: React.FocusEventHandler<HTMLTextAreaElement>;
168-
}) => (
169-
<textarea data-testid="textarea" value={value} defaultValue={defaultValue} onChange={onChange} onKeyDown={onKeyDown} onFocus={onFocus} placeholder={placeholder} onBlur={onBlur} />
170-
),
172+
}>(function Textarea({ // eslint-disable-line prefer-arrow-callback
173+
value, onChange, onKeyDown, onFocus, placeholder, onBlur, defaultValue,
174+
}, ref) {
175+
return <textarea ref={ref} data-testid="textarea" value={value} defaultValue={defaultValue} onChange={onChange} onKeyDown={onKeyDown} onFocus={onFocus} placeholder={placeholder} onBlur={onBlur} />;
176+
}),
171177
TextInput: ({ placeholder, value }: { placeholder?: string; value?: string }) => <input placeholder={placeholder} defaultValue={value} />,
172178
Tooltip: ({ children, label }: { children: ReactNode; label?: ReactNode }) => <div title={String(label)}>{children}</div>,
173179
useCombobox: () => ({ toggleDropdown: vi.fn(), openDropdown: vi.fn(), closeDropdown: vi.fn() }),

src/analysis/tests/StudyAnalysisTabs.spec.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from 'react';
1+
import { forwardRef, ReactNode } from 'react';
22
import { renderToStaticMarkup } from 'react-dom/server';
33
import {
44
render, act, cleanup, screen, waitFor, fireEvent,
@@ -141,7 +141,9 @@ vi.mock('@mantine/core', () => ({
141141
Flex: ({ children }: { children: ReactNode }) => <div>{children}</div>,
142142
Group: ({ children }: { children: ReactNode }) => <div>{children}</div>,
143143
LoadingOverlay: () => null,
144-
Stack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
144+
Stack: forwardRef<HTMLDivElement, { children: ReactNode }>(function Stack({ children }, ref) { // eslint-disable-line prefer-arrow-callback
145+
return <div ref={ref}>{children}</div>;
146+
}),
145147
Tabs: Object.assign(
146148
({ children }: { children: ReactNode }) => <div>{children}</div>,
147149
{

0 commit comments

Comments
 (0)