sky.html: snap-binary aperture scroll (wheel ↔ form, full aperture each); SAVE SKY animates scrollTop back to 0 — TDD
post-save the .sky-page aperture flips into scroll-snap-y-mandatory mode: wheel-col & form-col each fill the aperture & carry scroll-snap-align:start, so vertical scroll toggles between them rather than free-flowing through both. Modal-body uses display:contents so the cols become direct flex children of .sky-page (where min-height:100% resolves against the explicit aperture height); wheel-col's aspect-ratio/max-height caps are released under body.sky-saved so the section actually fills the aperture instead of clipping at 480px. SAVE SKY's success branch calls _scrollApertureToTop(), a 280ms RAF loop w. ease-out cubic so the user lands back on the wheel after confirming from the form section. New FT class MySkyApertureSnapScrollTest covers (T1) snap-type:y mandatory + scroll-snap-align:start on both cols, (T2) scrollTop returns to 0 after SAVE SKY click; both red before the SCSS+JS, green after. Snap behavior is gated on body.sky-saved (set by sky_view based on user.sky_chart_data) so the pre-save form-only flow is untouched. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -374,7 +374,7 @@ def sky_view(request):
|
||||
"saved_birth_lat": request.user.sky_birth_lat,
|
||||
"saved_birth_lon": request.user.sky_birth_lon,
|
||||
"saved_birth_tz": request.user.sky_birth_tz,
|
||||
"page_class": "page-sky",
|
||||
"page_class": "page-sky" + (" sky-saved" if chart_data else ""),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -372,6 +372,101 @@ class MySkyTimezoneRefreshTest(FunctionalTest):
|
||||
))
|
||||
|
||||
|
||||
class MySkyApertureSnapScrollTest(FunctionalTest):
|
||||
"""Once sky data is saved, the .sky-page aperture is a snap-binary scroller
|
||||
(wheel section + form section, each filling the aperture). Clicking SAVE
|
||||
SKY animates the aperture back to the top (the wheel)."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
Applet.objects.get_or_create(
|
||||
slug="my-sky",
|
||||
defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
|
||||
)
|
||||
self.gamer = User.objects.create(email="stargazer@test.io")
|
||||
self.gamer.sky_chart_data = _CHART_FIXTURE
|
||||
self.gamer.sky_birth_place = "Baltimore, MD, US"
|
||||
self.gamer.sky_birth_tz = "America/New_York"
|
||||
self.gamer.sky_birth_lat = 39.2904
|
||||
self.gamer.sky_birth_lon = -76.6122
|
||||
self.gamer.sky_birth_dt = datetime(
|
||||
1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
|
||||
)
|
||||
self.gamer.save()
|
||||
self.sky_url = self.live_server_url + "/dashboard/sky/"
|
||||
|
||||
# ── T1 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_aperture_has_snap_y_mandatory_when_sky_saved(self):
|
||||
"""When sky is saved, the .sky-page aperture has scroll-snap-type:y mandatory
|
||||
and both wheel-col & form-col have scroll-snap-align:start. Without these
|
||||
the layout is a free scroll instead of the binary wheel<->form toggle."""
|
||||
self.browser.set_window_size(820, 520)
|
||||
self.create_pre_authenticated_session("stargazer@test.io")
|
||||
self.browser.get(self.sky_url)
|
||||
self.wait_for(lambda: self.browser.find_element(By.CLASS_NAME, "sky-page"))
|
||||
|
||||
styles = self.browser.execute_script("""
|
||||
const ap = document.querySelector('.sky-page');
|
||||
const wheel = document.querySelector('.sky-page .sky-wheel-col');
|
||||
const form = document.querySelector('.sky-page .sky-form-col');
|
||||
return {
|
||||
snapType: getComputedStyle(ap).scrollSnapType,
|
||||
wheelAlign: getComputedStyle(wheel).scrollSnapAlign,
|
||||
formAlign: getComputedStyle(form).scrollSnapAlign,
|
||||
};
|
||||
""")
|
||||
self.assertIn("y", styles["snapType"])
|
||||
self.assertIn("mandatory", styles["snapType"])
|
||||
self.assertEqual(styles["wheelAlign"], "start")
|
||||
self.assertEqual(styles["formAlign"], "start")
|
||||
|
||||
# ── T2 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_save_sky_scrolls_aperture_back_to_top(self):
|
||||
"""Clicking SAVE SKY from the form section animates the aperture's
|
||||
scrollTop back to 0 (the wheel)."""
|
||||
self.browser.set_window_size(820, 520)
|
||||
self.create_pre_authenticated_session("stargazer@test.io")
|
||||
self.browser.get(self.sky_url)
|
||||
self.wait_for(lambda: self.browser.find_element(By.CLASS_NAME, "sky-page"))
|
||||
|
||||
# Mock /sky/save so the click resolves without real server work
|
||||
self.browser.execute_script("""
|
||||
window._origFetch = window.fetch;
|
||||
window.fetch = function(url, opts) {
|
||||
if (typeof url === 'string' && url.includes('/sky/save')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({saved: true}),
|
||||
});
|
||||
}
|
||||
return window._origFetch(url, opts);
|
||||
};
|
||||
""")
|
||||
|
||||
# Scroll the aperture down to the form section
|
||||
self.browser.execute_script(
|
||||
"document.querySelector('.sky-page').scrollTop = 9999;"
|
||||
)
|
||||
self.wait_for(lambda: self.assertGreater(
|
||||
self.browser.execute_script(
|
||||
"return document.querySelector('.sky-page').scrollTop;"
|
||||
),
|
||||
10,
|
||||
))
|
||||
|
||||
self.browser.find_element(By.ID, "id_sky_confirm").click()
|
||||
|
||||
# After save resolves, aperture scrollTop animates back to 0
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.execute_script(
|
||||
"return Math.round(document.querySelector('.sky-page').scrollTop);"
|
||||
),
|
||||
0,
|
||||
))
|
||||
|
||||
|
||||
class MySkyWheelConjunctionTest(FunctionalTest):
|
||||
"""Tick lines, z-raise, and dual tooltip for conjunct planets."""
|
||||
|
||||
|
||||
@@ -963,6 +963,48 @@ body.page-sky {
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
// ── Snap-binary aperture (post-save) ──────────────────────────────────────────
|
||||
// Once a sky is saved, the .sky-page aperture flips into scroll-snap mode:
|
||||
// the wheel section + form section each fill the aperture, so scrolling toggles
|
||||
// between them rather than free-flowing. SAVE SKY (in sky.html's click handler)
|
||||
// animates the aperture back to the top after a successful save.
|
||||
//
|
||||
// modal-body keeps height:100% (= aperture height), so its two flex children
|
||||
// using min-height:100% each resolve to a full aperture each → modal-body's
|
||||
// content overflows itself → .sky-page (overflow-y:auto) becomes the scroller.
|
||||
|
||||
body.sky-saved {
|
||||
.sky-page {
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
// modal-body acts as a layout pass-through so the wheel & form cols become
|
||||
// direct flex children of .sky-page, where `min-height: 100%` resolves
|
||||
// against the aperture height (.sky-page has flex:1 + min-height:0 so its
|
||||
// height is explicit in the parent flex column).
|
||||
.sky-page .sky-modal-body {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sky-page .sky-wheel-col,
|
||||
.sky-page .sky-form-col {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
min-height: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
// Release the wheel-col aspect-ratio cap so the section fills the aperture;
|
||||
// .sky-svg inside still renders square (its own aspect-ratio:1/1) and stays
|
||||
// centered via the col's align-items:center.
|
||||
.sky-page .sky-wheel-col {
|
||||
aspect-ratio: auto;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sidebar z-index sink (landscape sidebars must go below backdrop) ───────────
|
||||
|
||||
@media (orientation: landscape) {
|
||||
|
||||
@@ -304,13 +304,40 @@
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
})
|
||||
.then(data => { setStatus('Sky saved!'); Note.handleSaveResponse(data); })
|
||||
.then(data => {
|
||||
setStatus('Sky saved!');
|
||||
Note.handleSaveResponse(data);
|
||||
_scrollApertureToTop();
|
||||
})
|
||||
.catch(err => {
|
||||
setStatus(`Save failed: ${err.message}`, 'error');
|
||||
confirmBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Snap-back scroll on save ────────────────────────────────────────────
|
||||
// .sky-page is scroll-snap-y-mandatory (post-save). After SAVE SKY the user
|
||||
// should land back on the wheel section even if they clicked from the form
|
||||
// section. cubic-bezier(0.2, 0.9, 0.4, 1) — fast start, gentle settle.
|
||||
|
||||
function _scrollApertureToTop() {
|
||||
if (overlay.scrollTop === 0) return;
|
||||
const start = overlay.scrollTop;
|
||||
const startTime = performance.now();
|
||||
const DURATION = 280;
|
||||
const ease = (t) => {
|
||||
// ease-out cubic — visually equivalent to cubic-bezier(0, 0, 0.4, 1).
|
||||
const u = 1 - t;
|
||||
return 1 - u * u * u;
|
||||
};
|
||||
function step(now) {
|
||||
const t = Math.min(1, (now - startTime) / DURATION);
|
||||
overlay.scrollTop = Math.max(0, start * (1 - ease(t)));
|
||||
if (t < 1) requestAnimationFrame(step);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
// ── CSRF ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function _getCsrf() {
|
||||
|
||||
Reference in New Issue
Block a user