Fix trapped scrollbars app-wide, keep the week calendar as its own scroll

Several overflow-x-auto wrappers (table cards, a text preview) got an
accidental *vertical* scrollbar too -- per the CSS overflow spec, setting
only overflow-x to a non-visible value forces the other axis to compute as
auto if left unset, so any of these taller than the viewport were trapped
scrolling independently of the page (Safari showed it plainly; other
browsers hid it more subtly). Fixed everywhere with overflow-y-visible,
except the week calendar.

The week grid genuinely needs to stay its own bounded, contained scroll:
it always spans the full 24h day (never clipped, by design), so folding it
into the page's own scroll would mean scrolling past a screenful of empty
early hours most weeks. Instead it's capped to 70vh with a real
overflow-y-auto, and a small script scrolls it to just before the week's
first event on load (events.services.calendar.week_grid now reports
first_event_hour) -- no more landing on an empty view by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-22 19:13:28 +02:00
parent ae8e4c411d
commit 86ea8d1b15
13 changed files with 89 additions and 11 deletions

View File

@@ -29,6 +29,6 @@
<iframe class="h-[560px] w-full rounded-lg border border-line bg-white" src="{{ render_url }}" loading="lazy" title="{{ preview.label }}"></iframe>
</div>
{% if show_text_toggle %}
<pre class="hidden overflow-x-auto p-4.5 font-mono text-sm whitespace-pre-wrap text-ink" data-view-panel="text">{{ preview.text }}</pre>
<pre class="hidden overflow-x-auto overflow-y-visible p-4.5 font-mono text-sm whitespace-pre-wrap text-ink" data-view-panel="text">{{ preview.text }}</pre>
{% endif %}
</div>

View File

@@ -17,7 +17,7 @@
</div>
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<tbody>
{% for object in object_list %}

View File

@@ -68,7 +68,7 @@
{% block panel %}
{% if view_mode == "list" %}
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>
@@ -158,7 +158,17 @@
<div class="border-b border-line px-4 py-2.5 font-display text-sm font-extrabold tracking-[.08em] text-ink uppercase">
{% blocktrans with start=calendar.week_start|date:"j M" end=calendar.week_end|date:"j M Y" %}{{ start }} {{ end }}{% endblocktrans %}
</div>
<div class="overflow-x-auto">
{% comment %}
Deliberately its own bounded, contained scroll (max-h + overflow-y-auto)
rather than riding the page's own scroll like everywhere else in this app
(management/base.html's own comment) -- the grid always spans the full
24h day (never clipped, see DEFAULT_DAY_START_HOUR/END_HOUR's own
comment), so inlining all of it into the page would mean scrolling past
a screenful of empty small-hours before reaching any events most weeks.
extra_body's own script scrolls this container to just before the
week's first event on load, for the same reason.
{% endcomment %}
<div id="cal-week-scroll" class="max-h-[70vh] overflow-x-auto overflow-y-auto" data-first-event-hour="{{ calendar.first_event_hour|default_if_none:"" }}" data-day-start-hour="{{ calendar.day_start_hour }}" data-day-end-hour="{{ calendar.day_end_hour }}">
{% with hours_span=calendar.hours|length|add:"-1" %}
<div class="cal-week-inner">
<div class="cal-week-header">
@@ -277,3 +287,25 @@
{% endif %}
{% endif %}
{% endblock panel %}
{% block extra_body %}
{% if calendar_range == "week" %}
<script>
(() => {
// Scrolls #cal-week-scroll to just before the week's first event on
// load -- the grid always spans the full 24h day (see week_grid's own
// comment), so without this most weeks would open on an empty
// small-hours view. No-op when the week has no events at all
// (data-first-event-hour is blank then -- nothing to reveal either way).
const container = document.getElementById("cal-week-scroll");
const body = container?.querySelector(".cal-week-body");
if (!container || !body || container.dataset.firstEventHour === "") return;
const dayStart = Number(container.dataset.dayStartHour);
const dayEnd = Number(container.dataset.dayEndHour);
const targetHour = Math.max(Number(container.dataset.firstEventHour) - 1, dayStart);
container.scrollTop = ((targetHour - dayStart) / (dayEnd - dayStart)) * body.offsetHeight;
})();
</script>
{% endif %}
{% endblock extra_body %}

View File

@@ -72,7 +72,7 @@
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "calendar" size=18 %} {% trans "Occurrences" %}</h2>
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>

View File

@@ -9,7 +9,7 @@
{% block panel %}
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>

View File

@@ -122,7 +122,7 @@
</div>
</div>
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>

View File

@@ -9,7 +9,7 @@
{% block panel %}
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>

View File

@@ -18,7 +18,7 @@
<h2 class="card-title text-base">{% lucide "plus" size=18 %} {% blocktrans count counter=plan.to_create|length %}{{ counter }} game to create{% plural %}{{ counter }} games to create{% endblocktrans %}</h2>
{% if plan.to_create %}
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>
@@ -69,7 +69,7 @@
<h2 class="card-title text-base">{% lucide "refresh-cw" size=18 %} {% blocktrans count counter=plan.to_update|length %}{{ counter }} game to update{% plural %}{{ counter }} games to update{% endblocktrans %}</h2>
{% if plan.to_update %}
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<table class="table">
<thead>
<tr>

View File

@@ -5434,6 +5434,24 @@ class EventManagementTests(ManagementTestBase):
self.assertContains(response, 'aria-label="Part of a series"', count=1)
def test_the_week_calendar_carries_the_first_event_hour_for_the_initial_scroll(self):
local_today = timezone.localdate()
start = timezone.make_aware(datetime.datetime.combine(local_today, datetime.time(18, 0)))
event = Event.objects.create(club=self.club, title="Evening practice", start=start)
event.teams.add(self.own_team)
self.client.force_login(self.own_team_coach)
response = self.club_get("event_list") # default view=calendar, range=week
self.assertContains(response, 'data-first-event-hour="18"')
def test_the_week_calendar_first_event_hour_is_blank_with_no_events(self):
self.client.force_login(self.own_team_coach)
response = self.club_get("event_list", params={"date": "2030-01-07"}) # a Monday with nothing scheduled
self.assertContains(response, 'data-first-event-hour=""')
def test_the_month_calendar_marks_a_series_occurrence_with_the_repeat_icon(self):
series = EventSeries.objects.create(club=self.club, title="Weekly Training", kind=Event.EventKind.TRAINING, dtstart=timezone.now(), rrule="FREQ=WEEKLY;COUNT=1")
occurrence = Event.objects.create(club=self.club, title="Weekly Training", start=timezone.now(), series=series)