seed-map rim placement rides the pointer pair, not click — fixes Uranus never placing on touch screens — TDD
- iOS withholds a tap's synthesized click whenever a document-level mousemove/mouseover handler (the room page runs several tooltip machines) mutates the DOM — the wedge lit its :hover feedback but _onPickSign never fired on mobile staging - drawRim placement mode now binds pointerdown→pointerup w. a 10px slop radius (drag/scroll intent fires pointercancel or drifts past it); click stays only as the no-PointerEvent fallback — no double- pick on desktop - SkyWheelSpec R7 retold as the pure pointer tap (no click), + R7b compat-click no-double-pick + R7c drag-cancel; R9 tap-ified - seed map FT dispatches the pointer pair on the wedge [[project-voronoi-spec]] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1503,12 +1503,38 @@ const SkyWheel = (() => {
|
||||
.attr('class', 'nw-sign-group')
|
||||
.attr('data-sign-name', sign.name);
|
||||
// Placement mode (Set the Game Clock): the wedge is a placement target.
|
||||
// Placement rides the POINTER timeline (down→up tap with a slop radius),
|
||||
// NOT the click event: on iOS a tap's synthesized click is withheld
|
||||
// whenever a document-level mousemove/mouseover handler (the room page
|
||||
// runs several tooltip machines) mutates the DOM — the wedge lit its
|
||||
// :hover feedback but never placed on touch screens. Pointer events fire
|
||||
// on the raw input timeline, immune to that compat-mouse suppression; a
|
||||
// scroll/drag intent either fires pointercancel (no up) or drifts past
|
||||
// the slop radius. Mice fire the same pair, so desktop needs no click
|
||||
// handler (binding one would double-pick).
|
||||
if (opts.placeable) {
|
||||
slice.classed('nw-sign--placeable', true).style('cursor', 'pointer')
|
||||
.on('click', function (event) {
|
||||
slice.classed('nw-sign--placeable', true).style('cursor', 'pointer');
|
||||
const pick = function (event) {
|
||||
event.stopPropagation();
|
||||
if (opts.onPickSign) opts.onPickSign(sign.name);
|
||||
};
|
||||
if (window.PointerEvent) {
|
||||
let downX = 0, downY = 0, downId = null;
|
||||
slice
|
||||
.on('pointerdown', function (event) {
|
||||
downId = event.pointerId;
|
||||
downX = event.clientX;
|
||||
downY = event.clientY;
|
||||
})
|
||||
.on('pointerup', function (event) {
|
||||
if (downId === null || event.pointerId !== downId) return;
|
||||
downId = null;
|
||||
if (Math.hypot(event.clientX - downX, event.clientY - downY) > 10) return;
|
||||
pick(event);
|
||||
});
|
||||
} else {
|
||||
slice.on('click', pick);
|
||||
}
|
||||
}
|
||||
slice.append('path')
|
||||
.attr('transform', `translate(${cx},${cy})`)
|
||||
|
||||
@@ -286,10 +286,16 @@ class SeedMapClockTest(FunctionalTest):
|
||||
))
|
||||
self.assertEqual(self._uranus_count(), 0)
|
||||
|
||||
# Clicking the Aquarius sign wedge places Uranus there. SVG <g> elements
|
||||
# don't expose .click() in Firefox — dispatch the event (TDD skill).
|
||||
# Tapping the Aquarius sign wedge places Uranus there. Placement rides
|
||||
# the pointerdown→pointerup pair, not click (iOS withholds the tap-
|
||||
# synthesized click — see SkyWheelSpec R7). SVG <g> elements don't
|
||||
# expose .click() in Firefox anyway — dispatch the events (TDD skill).
|
||||
self.browser.execute_script(
|
||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||
"var el = arguments[0];"
|
||||
"['pointerdown', 'pointerup'].forEach(function (t) {"
|
||||
" el.dispatchEvent(new PointerEvent(t,"
|
||||
" {bubbles: true, pointerId: 1, clientX: 10, clientY: 10}));"
|
||||
"})",
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_seed_wheel_svg [data-sign-name='Aquarius']"),
|
||||
)
|
||||
|
||||
@@ -1148,26 +1148,63 @@ describe("SkyWheel — drawRim (SEED MAP shared rim)", () => {
|
||||
|
||||
// ── Placement mode (Set the Game Clock ritual) ──────────────────────────
|
||||
// drawRim(svg, data, {placeable, onPickSign}) turns the sign wedges into
|
||||
// placement targets: the gamer whose turn it is clicks a sign to place the
|
||||
// placement targets: the gamer whose turn it is taps a sign to place the
|
||||
// active planet there. Still singleton-pure — no SkyWheel module writes.
|
||||
//
|
||||
// Placement rides the POINTER timeline (pointerdown→pointerup tap), never
|
||||
// the click event: on iOS a tap's synthesized click is WITHHELD whenever a
|
||||
// document-level mousemove/mouseover handler (the room page runs several
|
||||
// tooltip machines) mutates the DOM — the wedge lit its :hover feedback
|
||||
// but Uranus never placed on touch screens.
|
||||
|
||||
it("R7: placement mode makes the sign wedges clickable and reports the picked sign", () => {
|
||||
// A tap: down + up on the wedge, no wander — and crucially NO click event,
|
||||
// exactly what a suppressed-click mobile tap delivers.
|
||||
function tap(el, opts) {
|
||||
opts = opts || {};
|
||||
const downX = opts.downX === undefined ? 100 : opts.downX;
|
||||
const upX = opts.upX === undefined ? downX : opts.upX;
|
||||
el.dispatchEvent(new PointerEvent("pointerdown",
|
||||
{ pointerId: 7, clientX: downX, clientY: 100, bubbles: true }));
|
||||
el.dispatchEvent(new PointerEvent("pointerup",
|
||||
{ pointerId: 7, clientX: upX, clientY: 100, bubbles: true }));
|
||||
}
|
||||
|
||||
it("R7: placement mode reports the tapped sign from the pointer pair alone (no click)", () => {
|
||||
let picked = null;
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: (s) => { picked = s; } });
|
||||
|
||||
const aqua = rimSvg.querySelector("[data-sign-name='Aquarius']");
|
||||
expect(aqua.classList.contains("nw-sign--placeable")).toBe(true);
|
||||
aqua.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
tap(aqua);
|
||||
expect(picked).toBe("Aquarius");
|
||||
});
|
||||
|
||||
it("R7b: a desktop click's compat event after the tap does not double-pick", () => {
|
||||
let picks = 0;
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: () => { picks++; } });
|
||||
|
||||
const aqua = rimSvg.querySelector("[data-sign-name='Aquarius']");
|
||||
tap(aqua);
|
||||
// A real mouse click follows its pointer pair with a click event.
|
||||
aqua.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(picks).toBe(1);
|
||||
});
|
||||
|
||||
it("R7c: a drag across the wedge (down→up beyond the slop radius) does not place", () => {
|
||||
let picked = null;
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: (s) => { picked = s; } });
|
||||
|
||||
tap(rimSvg.querySelector("[data-sign-name='Aquarius']"), { downX: 100, upX: 140 });
|
||||
expect(picked).toBeNull();
|
||||
});
|
||||
|
||||
it("R8: without placement opts the sign wedges stay inert (no placeable class)", () => {
|
||||
SkyWheel.drawRim(rimSvg, ROOM_SKY);
|
||||
const aqua = rimSvg.querySelector("[data-sign-name='Aquarius']");
|
||||
expect(aqua.classList.contains("nw-sign--placeable")).toBe(false);
|
||||
});
|
||||
|
||||
it("R9: a placement click never touches the interactive singleton", () => {
|
||||
it("R9: a placement tap never touches the interactive singleton", () => {
|
||||
skySvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
skySvg.setAttribute("id", "id_sky_svg");
|
||||
skySvg.style.width = "400px";
|
||||
@@ -1180,8 +1217,7 @@ describe("SkyWheel — drawRim (SEED MAP shared rim)", () => {
|
||||
SkyWheel.draw(skySvg, CONJUNCTION_CHART);
|
||||
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: () => {} });
|
||||
rimSvg.querySelector("[data-sign-name='Aries']")
|
||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
tap(rimSvg.querySelector("[data-sign-name='Aries']"));
|
||||
|
||||
// The interactive wheel's tooltip controls survive; no sign locked active.
|
||||
expect(tooltipEl.querySelector(".nw-tt-prv")).not.toBeNull();
|
||||
|
||||
@@ -1148,26 +1148,63 @@ describe("SkyWheel — drawRim (SEED MAP shared rim)", () => {
|
||||
|
||||
// ── Placement mode (Set the Game Clock ritual) ──────────────────────────
|
||||
// drawRim(svg, data, {placeable, onPickSign}) turns the sign wedges into
|
||||
// placement targets: the gamer whose turn it is clicks a sign to place the
|
||||
// placement targets: the gamer whose turn it is taps a sign to place the
|
||||
// active planet there. Still singleton-pure — no SkyWheel module writes.
|
||||
//
|
||||
// Placement rides the POINTER timeline (pointerdown→pointerup tap), never
|
||||
// the click event: on iOS a tap's synthesized click is WITHHELD whenever a
|
||||
// document-level mousemove/mouseover handler (the room page runs several
|
||||
// tooltip machines) mutates the DOM — the wedge lit its :hover feedback
|
||||
// but Uranus never placed on touch screens.
|
||||
|
||||
it("R7: placement mode makes the sign wedges clickable and reports the picked sign", () => {
|
||||
// A tap: down + up on the wedge, no wander — and crucially NO click event,
|
||||
// exactly what a suppressed-click mobile tap delivers.
|
||||
function tap(el, opts) {
|
||||
opts = opts || {};
|
||||
const downX = opts.downX === undefined ? 100 : opts.downX;
|
||||
const upX = opts.upX === undefined ? downX : opts.upX;
|
||||
el.dispatchEvent(new PointerEvent("pointerdown",
|
||||
{ pointerId: 7, clientX: downX, clientY: 100, bubbles: true }));
|
||||
el.dispatchEvent(new PointerEvent("pointerup",
|
||||
{ pointerId: 7, clientX: upX, clientY: 100, bubbles: true }));
|
||||
}
|
||||
|
||||
it("R7: placement mode reports the tapped sign from the pointer pair alone (no click)", () => {
|
||||
let picked = null;
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: (s) => { picked = s; } });
|
||||
|
||||
const aqua = rimSvg.querySelector("[data-sign-name='Aquarius']");
|
||||
expect(aqua.classList.contains("nw-sign--placeable")).toBe(true);
|
||||
aqua.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
tap(aqua);
|
||||
expect(picked).toBe("Aquarius");
|
||||
});
|
||||
|
||||
it("R7b: a desktop click's compat event after the tap does not double-pick", () => {
|
||||
let picks = 0;
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: () => { picks++; } });
|
||||
|
||||
const aqua = rimSvg.querySelector("[data-sign-name='Aquarius']");
|
||||
tap(aqua);
|
||||
// A real mouse click follows its pointer pair with a click event.
|
||||
aqua.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(picks).toBe(1);
|
||||
});
|
||||
|
||||
it("R7c: a drag across the wedge (down→up beyond the slop radius) does not place", () => {
|
||||
let picked = null;
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: (s) => { picked = s; } });
|
||||
|
||||
tap(rimSvg.querySelector("[data-sign-name='Aquarius']"), { downX: 100, upX: 140 });
|
||||
expect(picked).toBeNull();
|
||||
});
|
||||
|
||||
it("R8: without placement opts the sign wedges stay inert (no placeable class)", () => {
|
||||
SkyWheel.drawRim(rimSvg, ROOM_SKY);
|
||||
const aqua = rimSvg.querySelector("[data-sign-name='Aquarius']");
|
||||
expect(aqua.classList.contains("nw-sign--placeable")).toBe(false);
|
||||
});
|
||||
|
||||
it("R9: a placement click never touches the interactive singleton", () => {
|
||||
it("R9: a placement tap never touches the interactive singleton", () => {
|
||||
skySvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
skySvg.setAttribute("id", "id_sky_svg");
|
||||
skySvg.style.width = "400px";
|
||||
@@ -1180,8 +1217,7 @@ describe("SkyWheel — drawRim (SEED MAP shared rim)", () => {
|
||||
SkyWheel.draw(skySvg, CONJUNCTION_CHART);
|
||||
|
||||
SkyWheel.drawRim(rimSvg, null, { placeable: "Uranus", onPickSign: () => {} });
|
||||
rimSvg.querySelector("[data-sign-name='Aries']")
|
||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
tap(rimSvg.querySelector("[data-sign-name='Aries']"));
|
||||
|
||||
// The interactive wheel's tooltip controls survive; no sign locked active.
|
||||
expect(tooltipEl.querySelector(".nw-tt-prv")).not.toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user