tray tooltips: tilt persists while portal is open; PRV|NXT pinned to corners — TDD
- TrayTooltip adds .tt-active to the .tray-role-card / .tray-sig-card cell while its tooltip is open & removes it on _hide. The hover-tilt selectors gain .tt-active alongside :hover, :focus so the card stays tilted while the user is hovering the portal itself rather than the cell. - #id_tooltip_portal: .fyi-prev / .fyi-next pinned to the bottom corners w. 1rem outside the panel (bottom: -1rem; left/right: -1rem) — same anchor the @stat-block-shared mixin uses for fan / sig / sea, restated here since the portal isn't covered by that mixin. - 2 new TrayTooltipSpec specs (.tt-active added on hover, removed on _hide; for both role & sig branches). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
var TrayTooltip = (function () {
|
var TrayTooltip = (function () {
|
||||||
var _portal = null;
|
var _portal = null;
|
||||||
var _activeTrig = null;
|
var _activeTrig = null;
|
||||||
|
var _activeCell = null; // .tray-role-card / .tray-sig-card holding the tilt class
|
||||||
var _activeKind = null; // "role" | "sig"
|
var _activeKind = null; // "role" | "sig"
|
||||||
var _infoData = [];
|
var _infoData = [];
|
||||||
var _infoIdx = 0;
|
var _infoIdx = 0;
|
||||||
@@ -37,12 +38,20 @@ var TrayTooltip = (function () {
|
|||||||
_portal.classList.remove("active");
|
_portal.classList.remove("active");
|
||||||
_portal.style.display = "none";
|
_portal.style.display = "none";
|
||||||
_portal.innerHTML = "";
|
_portal.innerHTML = "";
|
||||||
|
if (_activeCell) _activeCell.classList.remove("tt-active");
|
||||||
|
_activeCell = null;
|
||||||
_activeTrig = null;
|
_activeTrig = null;
|
||||||
_activeKind = null;
|
_activeKind = null;
|
||||||
_infoData = [];
|
_infoData = [];
|
||||||
_infoIdx = 0;
|
_infoIdx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the .tray-role-card / .tray-sig-card ancestor of a trigger so we
|
||||||
|
// can keep its hover-tilt locked while the portal is open.
|
||||||
|
function _cellOf(triggerEl) {
|
||||||
|
return triggerEl.closest && triggerEl.closest(".tray-role-card, .tray-sig-card");
|
||||||
|
}
|
||||||
|
|
||||||
function _position(triggerEl) {
|
function _position(triggerEl) {
|
||||||
var triggerRect = triggerEl.getBoundingClientRect();
|
var triggerRect = triggerEl.getBoundingClientRect();
|
||||||
var halfW = _portal.offsetWidth / 2;
|
var halfW = _portal.offsetWidth / 2;
|
||||||
@@ -69,6 +78,8 @@ var TrayTooltip = (function () {
|
|||||||
_portal.style.display = "block";
|
_portal.style.display = "block";
|
||||||
_activeTrig = triggerEl;
|
_activeTrig = triggerEl;
|
||||||
_activeKind = "role";
|
_activeKind = "role";
|
||||||
|
_activeCell = _cellOf(triggerEl);
|
||||||
|
if (_activeCell) _activeCell.classList.add("tt-active");
|
||||||
_position(triggerEl);
|
_position(triggerEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +118,8 @@ var TrayTooltip = (function () {
|
|||||||
_portal.style.display = "block";
|
_portal.style.display = "block";
|
||||||
_activeTrig = triggerEl;
|
_activeTrig = triggerEl;
|
||||||
_activeKind = "sig";
|
_activeKind = "sig";
|
||||||
|
_activeCell = _cellOf(triggerEl);
|
||||||
|
if (_activeCell) _activeCell.classList.add("tt-active");
|
||||||
|
|
||||||
_portal.querySelector(".fyi-prev").addEventListener("click", function (e) {
|
_portal.querySelector(".fyi-prev").addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -138,6 +138,35 @@ describe("TrayTooltip", () => {
|
|||||||
// Clamping — portal left stays inside the viewport //
|
// Clamping — portal left stays inside the viewport //
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
// .tt-active on the cell — tilt persistence //
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
//
|
||||||
|
// The role-card / sig-card hover-tilt is keyed off :hover + :focus on the
|
||||||
|
// cell. While the portal is open the pointer is typically OFF the cell
|
||||||
|
// (hovering the portal itself), so :hover drops; if the cell never
|
||||||
|
// received focus, :focus is also absent and the tilt reverts even though
|
||||||
|
// the tooltip is still active. Solution: TrayTooltip adds .tt-active to
|
||||||
|
// the cell while its tooltip is open and removes it on _hide. SCSS
|
||||||
|
// includes .tt-active in the tilt selector list.
|
||||||
|
|
||||||
|
describe(".tt-active class on the cell", () => {
|
||||||
|
it("is added to the role-card cell on mouseenter and removed on hide", () => {
|
||||||
|
img.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(cell.classList.contains("tt-active")).toBe(true);
|
||||||
|
|
||||||
|
// outer beforeEach already stubs img.getBoundingClientRect; just
|
||||||
|
// pin the portal so the union test is deterministic.
|
||||||
|
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||||
|
left: 0, right: 10, top: 0, bottom: 10, width: 10, height: 10,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||||
|
bubbles: true, clientX: 9999, clientY: 9999,
|
||||||
|
}));
|
||||||
|
expect(cell.classList.contains("tt-active")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
// Phase 2 — sig-card branch //
|
// Phase 2 — sig-card branch //
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
@@ -249,6 +278,19 @@ describe("TrayTooltip", () => {
|
|||||||
expect(portal.classList.contains("active")).toBe(true);
|
expect(portal.classList.contains("active")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds .tt-active to the sig cell on hover and removes it on _hide", () => {
|
||||||
|
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(sigCell.classList.contains("tt-active")).toBe(true);
|
||||||
|
|
||||||
|
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||||
|
left: 0, right: 10, top: 0, bottom: 10, width: 10, height: 10,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||||
|
bubbles: true, clientX: 9999, clientY: 9999,
|
||||||
|
}));
|
||||||
|
expect(sigCell.classList.contains("tt-active")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears portal when pointer leaves trigger + portal + btn rect union", () => {
|
it("clears portal when pointer leaves trigger + portal + btn rect union", () => {
|
||||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||||
|
|||||||
@@ -138,6 +138,22 @@ body.page-gameboard {
|
|||||||
.btn { margin: 0; }
|
.btn { margin: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tray sig-card tooltip (Phase 2) — PRV / NXT btns pinned to the bottom
|
||||||
|
// corners of the portal, 1rem outside the panel so the btn centres land
|
||||||
|
// exactly on the corners. The shared @stat-block-shared mixin in
|
||||||
|
// _card-deck.scss already does this for fan / sig / sea contexts; the
|
||||||
|
// portal isn't covered by that mixin so we re-state the rules here.
|
||||||
|
.fyi-prev,
|
||||||
|
.fyi-next {
|
||||||
|
display: inline-flex;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1rem;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 70;
|
||||||
|
}
|
||||||
|
.fyi-prev { left: -1rem; }
|
||||||
|
.fyi-next { right: -1rem; }
|
||||||
|
|
||||||
&.active { display: block; }
|
&.active { display: block; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,9 +152,12 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hover/touch tilts the scrawl 7° counter-clockwise; :focus persists the
|
// Hover/touch tilts the scrawl 7° counter-clockwise; :focus persists the
|
||||||
// tilt after a click (cell receives tabindex="0" from tray.js).
|
// tilt after a click (cell receives tabindex="0" from tray.js); .tt-active
|
||||||
|
// (set by TrayTooltip while the portal is open for this cell) keeps the
|
||||||
|
// tilt while the user is hovering the portal itself rather than the cell.
|
||||||
&:hover > img,
|
&:hover > img,
|
||||||
&:focus > img {
|
&:focus > img,
|
||||||
|
&.tt-active > img {
|
||||||
rotate: -7deg;
|
rotate: -7deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +187,8 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this
|
|||||||
transition: rotate 0.25s ease;
|
transition: rotate 0.25s ease;
|
||||||
}
|
}
|
||||||
&:hover > .sig-stage-card,
|
&:hover > .sig-stage-card,
|
||||||
&:focus > .sig-stage-card {
|
&:focus > .sig-stage-card,
|
||||||
|
&.tt-active > .sig-stage-card {
|
||||||
rotate: 7deg;
|
rotate: 7deg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,35 @@ describe("TrayTooltip", () => {
|
|||||||
// Clamping — portal left stays inside the viewport //
|
// Clamping — portal left stays inside the viewport //
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
// .tt-active on the cell — tilt persistence //
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
//
|
||||||
|
// The role-card / sig-card hover-tilt is keyed off :hover + :focus on the
|
||||||
|
// cell. While the portal is open the pointer is typically OFF the cell
|
||||||
|
// (hovering the portal itself), so :hover drops; if the cell never
|
||||||
|
// received focus, :focus is also absent and the tilt reverts even though
|
||||||
|
// the tooltip is still active. Solution: TrayTooltip adds .tt-active to
|
||||||
|
// the cell while its tooltip is open and removes it on _hide. SCSS
|
||||||
|
// includes .tt-active in the tilt selector list.
|
||||||
|
|
||||||
|
describe(".tt-active class on the cell", () => {
|
||||||
|
it("is added to the role-card cell on mouseenter and removed on hide", () => {
|
||||||
|
img.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(cell.classList.contains("tt-active")).toBe(true);
|
||||||
|
|
||||||
|
// outer beforeEach already stubs img.getBoundingClientRect; just
|
||||||
|
// pin the portal so the union test is deterministic.
|
||||||
|
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||||
|
left: 0, right: 10, top: 0, bottom: 10, width: 10, height: 10,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||||
|
bubbles: true, clientX: 9999, clientY: 9999,
|
||||||
|
}));
|
||||||
|
expect(cell.classList.contains("tt-active")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
// Phase 2 — sig-card branch //
|
// Phase 2 — sig-card branch //
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
@@ -249,6 +278,19 @@ describe("TrayTooltip", () => {
|
|||||||
expect(portal.classList.contains("active")).toBe(true);
|
expect(portal.classList.contains("active")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds .tt-active to the sig cell on hover and removes it on _hide", () => {
|
||||||
|
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(sigCell.classList.contains("tt-active")).toBe(true);
|
||||||
|
|
||||||
|
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||||
|
left: 0, right: 10, top: 0, bottom: 10, width: 10, height: 10,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||||
|
bubbles: true, clientX: 9999, clientY: 9999,
|
||||||
|
}));
|
||||||
|
expect(sigCell.classList.contains("tt-active")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears portal when pointer leaves trigger + portal + btn rect union", () => {
|
it("clears portal when pointer leaves trigger + portal + btn rect union", () => {
|
||||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||||
|
|||||||
Reference in New Issue
Block a user