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:
@@ -158,6 +158,12 @@ def week_grid(events, week_start: datetime.date) -> dict:
|
|||||||
"hours": list(range(day_start_hour, day_end_hour + 1)),
|
"hours": list(range(day_start_hour, day_end_hour + 1)),
|
||||||
"day_start_hour": day_start_hour,
|
"day_start_hour": day_start_hour,
|
||||||
"day_end_hour": day_end_hour,
|
"day_end_hour": day_end_hour,
|
||||||
|
# The grid always spans the full day (see DEFAULT_DAY_START_HOUR/END_HOUR's
|
||||||
|
# own comment -- never clipped), so a plain "top of the grid" scroll position
|
||||||
|
# would default to an empty 00:00 view most weeks. The template scrolls its
|
||||||
|
# bounded viewport to just before this hour instead -- None when the week has
|
||||||
|
# no events at all, since there's nothing to reveal either way.
|
||||||
|
"first_event_hour": min((start.hour for _event, start, _end in spans), default=None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1708,6 +1708,22 @@ class CalendarGridTests(EventsTestBase):
|
|||||||
self.assertEqual(grid["day_start_hour"], 0)
|
self.assertEqual(grid["day_start_hour"], 0)
|
||||||
self.assertEqual(grid["day_end_hour"], 24)
|
self.assertEqual(grid["day_end_hour"], 24)
|
||||||
|
|
||||||
|
def test_week_grid_first_event_hour_is_the_earliest_events_start_hour(self):
|
||||||
|
monday = date(2026, 8, 17)
|
||||||
|
morning = self.make_event(title="Morning", start=self.at(monday, 9), end=self.at(monday, 10))
|
||||||
|
evening = self.make_event(title="Evening", start=self.at(monday + timedelta(days=2), 18), end=self.at(monday + timedelta(days=2), 19))
|
||||||
|
|
||||||
|
grid = week_grid([morning, evening], monday)
|
||||||
|
|
||||||
|
self.assertEqual(grid["first_event_hour"], 9)
|
||||||
|
|
||||||
|
def test_week_grid_first_event_hour_is_none_when_the_week_has_no_events(self):
|
||||||
|
monday = date(2026, 8, 17)
|
||||||
|
|
||||||
|
grid = week_grid([], monday)
|
||||||
|
|
||||||
|
self.assertIsNone(grid["first_event_hour"])
|
||||||
|
|
||||||
def test_week_grid_excludes_events_outside_the_week(self):
|
def test_week_grid_excludes_events_outside_the_week(self):
|
||||||
monday = date(2026, 8, 17)
|
monday = date(2026, 8, 17)
|
||||||
event = self.make_event(start=self.at(monday + timedelta(days=7), 10))
|
event = self.make_event(start=self.at(monday + timedelta(days=7), 10))
|
||||||
|
|||||||
@@ -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>
|
<iframe class="h-[560px] w-full rounded-lg border border-line bg-white" src="{{ render_url }}" loading="lazy" title="{{ preview.label }}"></iframe>
|
||||||
</div>
|
</div>
|
||||||
{% if show_text_toggle %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card overflow-hidden">
|
<div class="card overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for object in object_list %}
|
{% for object in object_list %}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% if view_mode == "list" %}
|
{% if view_mode == "list" %}
|
||||||
<div class="card overflow-hidden">
|
<div class="card overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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">
|
<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 %}
|
{% blocktrans with start=calendar.week_start|date:"j M" end=calendar.week_end|date:"j M Y" %}{{ start }} – {{ end }}{% endblocktrans %}
|
||||||
</div>
|
</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" %}
|
{% with hours_span=calendar.hours|length|add:"-1" %}
|
||||||
<div class="cal-week-inner">
|
<div class="cal-week-inner">
|
||||||
<div class="cal-week-header">
|
<div class="cal-week-header">
|
||||||
@@ -277,3 +287,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock panel %}
|
{% 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 %}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-base">{% lucide "calendar" size=18 %} {% trans "Occurrences" %}</h2>
|
<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">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<div class="card overflow-hidden">
|
<div class="card overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<div class="card overflow-hidden">
|
<div class="card overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -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>
|
<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 %}
|
{% if plan.to_create %}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
<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 %}
|
{% if plan.to_update %}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -5434,6 +5434,24 @@ class EventManagementTests(ManagementTestBase):
|
|||||||
|
|
||||||
self.assertContains(response, 'aria-label="Part of a series"', count=1)
|
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):
|
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")
|
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)
|
occurrence = Event.objects.create(club=self.club, title="Weekly Training", start=timezone.now(), series=series)
|
||||||
|
|||||||
@@ -4028,6 +4028,9 @@
|
|||||||
.max-h-96 {
|
.max-h-96 {
|
||||||
max-height: calc(var(--spacing) * 96);
|
max-height: calc(var(--spacing) * 96);
|
||||||
}
|
}
|
||||||
|
.max-h-\[70vh\] {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
.max-h-full {
|
.max-h-full {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
@@ -4450,6 +4453,9 @@
|
|||||||
.overflow-y-auto {
|
.overflow-y-auto {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.overflow-y-visible {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user