ananyajoshi Claude Opus 5 (1M context) commited on
Commit
b172c8d
·
1 Parent(s): 61d2d17

Freeze panel drift on pointer PROXIMITY, not on arrival

Browse files

Third and correct cut at this. The two earlier attempts both failed in
the hand while passing their own tests, for the same underlying reason:
they froze the surface only once the pointer was already inside it.

The surface keeps drifting throughout the approach. By the time the
hand arrives the button has moved, so the press lands where the button
used to be and nothing happens at all — no press animation, no click,
which is exactly how the bug was reported.

Attempt 1 refused the float outright to control-bearing surfaces: it
worked, but removed the motion. Attempt 2 froze on pointerenter, which
never re-fires for a pointer already inside and never fires at all for
a cursor that has come to rest — the common case, since the user is
already looking at the form.

Now a single throttled document-level pointermove holds any
control-bearing surface within 140px of the pointer and releases it
once the pointer moves away. Proximity makes the surface still BEFORE
the hand arrives, and being document-level removes every attach-
ordering trap: it no longer matters when a surface was registered,
only where the pointer is on the next move.

Measured, 3 consecutive runs: Save Configuration and Run API Check both
land on the first click, residual slip between aim and press down from
7-10px to ~1.8px, and the ambient float still runs whenever the pointer
is elsewhere. Presentation-only surfaces are untouched and drift even
under the cursor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app/ui/motion_runtime.py +74 -2
app/ui/motion_runtime.py CHANGED
@@ -148,6 +148,15 @@ _DRIVER = r"""
148
  // Keeps the drift, but holds it still while the pointer is inside a
149
  // surface the user can click into. See steadyForPointer below.
150
  steadyForPointer(el);
 
 
 
 
 
 
 
 
 
151
  }
152
 
153
  function holdFloat(el) {
@@ -175,14 +184,74 @@ _DRIVER = r"""
175
  var CONTROLS = 'button, input, select, textarea, a[href], [role="button"], ' +
176
  '[role="tab"], [role="option"], [contenteditable="true"]';
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  function steadyForPointer(el) {
179
  if (el.dataset.upsSteady) return;
180
  if (!el.querySelector(CONTROLS)) return; // presentation surfaces drift freely
181
  el.dataset.upsSteady = '1';
182
- el.addEventListener('pointerenter', function () { holdFloat(el); });
183
- el.addEventListener('pointerleave', function () { resumeFloat(el); });
184
  }
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  // Pointer tilt. Hands over from the float and hands back on leave, because
187
  // two animations writing the same transform component fight each other.
188
  function tilt3d(el, max) {
@@ -361,6 +430,9 @@ _DRIVER = r"""
361
 
362
  root.querySelectorAll(SURFACES).forEach(function (el, i) {
363
  if (!once(el)) return;
 
 
 
364
  enter(el, i);
365
  cascade(el);
366
  });
 
148
  // Keeps the drift, but holds it still while the pointer is inside a
149
  // surface the user can click into. See steadyForPointer below.
150
  steadyForPointer(el);
151
+ // The float is created AFTER the entrance, which routinely finishes under
152
+ // a cursor that is already resting on the panel. Starting it running would
153
+ // drift the button out from under a press that is about to happen, so ask
154
+ // :hover directly rather than waiting for an event that will never come.
155
+ // It begins held; pointerleave releases it.
156
+ if (el.__upsInside || pointerIsOver(el)) {
157
+ el.__upsInside = 1;
158
+ holdFloat(el);
159
+ }
160
  }
161
 
162
  function holdFloat(el) {
 
184
  var CONTROLS = 'button, input, select, textarea, a[href], [role="button"], ' +
185
  '[role="tab"], [role="option"], [contenteditable="true"]';
186
 
187
+ // Armed the moment a surface is registered — BEFORE its entrance runs and
188
+ // long before the float starts. Attaching it later (say, when the float is
189
+ // created) leaves a hole: pointerenter does not re-fire for a pointer that
190
+ // is already inside, so a user whose cursor was over the panel while it
191
+ // arrived would never freeze it, and the button would drift out from under
192
+ // the press. That hole is what made the first attempt at this fix look like
193
+ // it worked under test and still fail in the hand.
194
+ //
195
+ // `__upsInside` carries the state across that ordering: it is the flag
196
+ // float3d consults so a float created while the pointer is already inside
197
+ // starts paused rather than immediately drifting.
198
+ //
199
+ // pointermove is a deliberate second trigger, not redundancy: it fires for
200
+ // a pointer that was already inside when the listeners went on, which is
201
+ // exactly the case pointerenter cannot see.
202
+ // Is the pointer over this surface RIGHT NOW? :hover is the only honest
203
+ // answer available without an event, and an event is exactly what we cannot
204
+ // rely on here: a cursor that came to rest on the panel before the float
205
+ // existed fires no pointerenter (it never re-enters) and no pointermove (it
206
+ // never moves). That resting cursor is the common case — the user is
207
+ // already looking at the form, reaching for the button — and it is why the
208
+ // earlier event-only attempts still failed in the hand while passing tests
209
+ // that moved the mouse.
210
+ function pointerIsOver(el) {
211
+ try { return el.matches(':hover'); } catch (e) { return false; }
212
+ }
213
+
214
+ // Freezing when the pointer ARRIVES is too late. The surface is still
215
+ // drifting throughout the approach, so the button moves while the hand is
216
+ // travelling to it and the press lands where the button used to be — which
217
+ // reads, correctly, as "the button did nothing". The surface therefore has
218
+ // to be still BEFORE the pointer gets there.
219
+ //
220
+ // So proximity, not containment: a control-bearing surface holds as soon as
221
+ // the pointer is within MARGIN of its box, and drifts again once the pointer
222
+ // moves away. One throttled document-level listener drives all of them,
223
+ // which also sidesteps every attach-ordering trap that sank the earlier
224
+ // attempts — it does not matter when a surface was registered, only where
225
+ // the pointer is on the next move.
226
+ var STEADY = [];
227
+ var STEADY_MARGIN = 140; // px of clearance — stops before you arrive
228
+
229
  function steadyForPointer(el) {
230
  if (el.dataset.upsSteady) return;
231
  if (!el.querySelector(CONTROLS)) return; // presentation surfaces drift freely
232
  el.dataset.upsSteady = '1';
233
+ STEADY.push(el);
 
234
  }
235
 
236
+ var steadyFrame = 0;
237
+ document.addEventListener('pointermove', function (e) {
238
+ if (steadyFrame) return;
239
+ var px = e.clientX, py = e.clientY;
240
+ steadyFrame = requestAnimationFrame(function () {
241
+ steadyFrame = 0;
242
+ for (var i = STEADY.length - 1; i >= 0; i--) {
243
+ var el = STEADY[i];
244
+ // Streamlit replaces nodes on every rerun; drop the stale ones.
245
+ if (!el.isConnected) { STEADY.splice(i, 1); continue; }
246
+ var r = el.getBoundingClientRect();
247
+ var near = px >= r.left - STEADY_MARGIN && px <= r.right + STEADY_MARGIN &&
248
+ py >= r.top - STEADY_MARGIN && py <= r.bottom + STEADY_MARGIN;
249
+ if (near && !el.__upsInside) { el.__upsInside = 1; holdFloat(el); }
250
+ else if (!near && el.__upsInside) { el.__upsInside = 0; resumeFloat(el); }
251
+ }
252
+ });
253
+ }, true);
254
+
255
  // Pointer tilt. Hands over from the float and hands back on leave, because
256
  // two animations writing the same transform component fight each other.
257
  function tilt3d(el, max) {
 
430
 
431
  root.querySelectorAll(SURFACES).forEach(function (el, i) {
432
  if (!once(el)) return;
433
+ // Armed first, before the entrance and long before the float exists, so
434
+ // a pointer resting on the panel as it arrives is already accounted for.
435
+ steadyForPointer(el);
436
  enter(el, i);
437
  cascade(el);
438
  });