feat(schedule): add horizontal scroll for week view on narrow viewports

- Week view now always shows all 7 days with navigation skipping full weeks
- Add horizontal scroll when viewport < 1900px to show hidden days
- Implement responsive column widths (180px desktop, 150px tablet, 120px mobile)
- Columns scale up with minmax() when extra space available
- Add visible themed scrollbar for better UX
- Simplify navigation logic: always skip 7 days forward/backward
- Simplify day generation: sequential days from reference date
- Update tests to match new 7-day navigation behavior
This commit is contained in:
Johannes Millan 2026-01-21 14:15:02 +01:00
parent 5c851e52d3
commit 7a98831835
5 changed files with 134 additions and 29 deletions

View file

@ -3,6 +3,84 @@
:host {
--schedule-time-width: 3em;
// Responsive day column widths
--schedule-day-width: 180px; // Default for desktop
@include mq(md, max) {
// Tablet
--schedule-day-width: 150px;
}
@include mq(xs, max) {
// Mobile
--schedule-day-width: 120px;
}
// Enable horizontal scroll when viewport is too small for 7 days
&[data-horizontal-scroll] {
overflow-x: auto;
overflow-y: visible;
display: block; // Change from default to allow proper overflow
// Show scrollbar on all platforms
&::-webkit-scrollbar {
height: 12px;
-webkit-appearance: none;
}
&::-webkit-scrollbar-track {
background: var(--bg-darker);
border-radius: 6px;
}
&::-webkit-scrollbar-thumb {
background: var(--extra-border-color);
border-radius: 6px;
border: 2px solid var(--bg-darker);
&:hover {
background: var(--text-color-muted);
}
}
// For Firefox
scrollbar-width: auto;
scrollbar-color: var(--extra-border-color) var(--bg-darker);
.week-header {
// Remove left/right constraints to allow horizontal scrolling
left: auto;
right: auto;
// Force width to match grid width
width: fit-content;
min-width: 100%;
}
.grid-container {
// Allow columns to scale up when space available, but enforce minimum for scroll
grid-template-columns: var(--schedule-time-width) repeat(
var(--nr-of-days),
minmax(var(--schedule-day-width), 1fr)
);
// Force grid to size based on content, not container
width: fit-content;
min-width: 100%;
// Disable centering to allow proper overflow
place-content: start;
}
.days {
// Match grid-container column widths exactly
grid-template-columns: var(--schedule-time-width) repeat(
var(--nr-of-days),
minmax(var(--schedule-day-width), 1fr)
);
// Ensure days container matches grid width
width: fit-content;
// Disable centering
place-content: start;
}
}
// State-based styles using host classes
&.isCtrlKeyPressed {

View file

@ -33,6 +33,9 @@ import { formatMonthDay } from '../../../util/format-month-day.util';
import { ScheduleWeekDragService } from './schedule-week-drag.service';
import { calculatePlaceholderForGridMove } from './schedule-week-placeholder.util';
import { truncate } from '../../../util/truncate';
import { fromEvent } from 'rxjs';
import { debounceTime, map, startWith } from 'rxjs/operators';
import { toSignal } from '@angular/core/rxjs-interop';
const D_HOURS = 24;
@ -58,6 +61,7 @@ const D_HOURS = 24;
'[class.is-not-dragging]': '!isDragging()',
'[class.is-resizing-event]': 'isAnyEventResizing()',
'[class]': 'dragEventTypeClass()',
'[attr.data-horizontal-scroll]': 'shouldEnableHorizontalScroll()',
},
})
export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy {
@ -65,6 +69,20 @@ export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy {
private _dateTimeFormatService = inject(DateTimeFormatService);
private _translateService = inject(TranslateService);
private _windowSize = toSignal(
fromEvent(window, 'resize').pipe(
startWith({ width: window.innerWidth }),
debounceTime(50),
map(() => ({ width: window.innerWidth })),
),
{ initialValue: { width: window.innerWidth } },
);
shouldEnableHorizontalScroll = computed(() => {
// Enable scroll when viewport is smaller than what's needed for 7 days
return this._windowSize().width < 1900;
});
isInPanel = input<boolean>(false);
events = input<ScheduleEvent[] | null>([]);
beyondBudget = input<ScheduleEvent[][] | null>([]);

View file

@ -75,10 +75,24 @@ describe('ScheduleService', () => {
expect(result[0]).toBe(expectedTodayStr);
});
it('should return days starting from referenceDate when provided', () => {
it('should return days starting from referenceDate', () => {
// Arrange
const nrOfDaysToShow = 3;
const referenceDate = new Date(2026, 0, 25); // Jan 25, 2026
const referenceDate = new Date(2028, 5, 15); // June 15, 2028
const expectedFirstDay = dateService.todayStr(referenceDate.getTime());
// Act
const result = service.getDaysToShow(nrOfDaysToShow, referenceDate);
// Assert
expect(result[0]).toBe(expectedFirstDay);
expect(result.length).toBe(3);
});
it('should return days starting from provided date even if today', () => {
// Arrange
const nrOfDaysToShow = 3;
const referenceDate = new Date();
const expectedFirstDay = dateService.todayStr(referenceDate.getTime());
// Act
@ -92,7 +106,8 @@ describe('ScheduleService', () => {
it('should return consecutive days from referenceDate', () => {
// Arrange
const nrOfDaysToShow = 7;
const referenceDate = new Date(2026, 0, 20); // Jan 20, 2026
// Use a future date to ensure not in current week
const referenceDate = new Date(2028, 0, 20); // Jan 20, 2028
// Act
const result = service.getDaysToShow(nrOfDaysToShow, referenceDate);
@ -112,7 +127,8 @@ describe('ScheduleService', () => {
it('should handle transition across months', () => {
// Arrange
const nrOfDaysToShow = 5;
const referenceDate = new Date(2026, 0, 30); // Jan 30, 2026
// Use a future date to ensure not in current week
const referenceDate = new Date(2028, 0, 30); // Jan 30, 2028
// Act
const result = service.getDaysToShow(nrOfDaysToShow, referenceDate);

View file

@ -213,7 +213,8 @@ describe('ScheduleComponent', () => {
// Assert
const newDate = component['_selectedDate']();
expect(newDate?.getDate()).toBe(13); // Jan 13, 2026
expect(newDate?.getDate()).toBe(13); // Jan 13, 2026 (20 - 7)
expect(newDate?.getHours()).toBe(0); // Normalized to midnight
});
it('should subtract 7 days from today when _selectedDate is null in week view', () => {
@ -228,6 +229,7 @@ describe('ScheduleComponent', () => {
// Assert
const newDate = component['_selectedDate']();
expect(newDate?.getDate()).toBe(expectedDate.getDate());
expect(newDate?.getHours()).toBe(0); // Normalized to midnight
});
it('should go to previous month in month view', () => {
@ -272,12 +274,15 @@ describe('ScheduleComponent', () => {
// Assert
const newDate = component['_selectedDate']();
expect(newDate?.getDate()).toBe(27); // Jan 27, 2026
expect(newDate?.getDate()).toBe(27); // Jan 27, 2026 (20 + 7)
expect(newDate?.getHours()).toBe(0); // Normalized to midnight
});
it('should add 7 days from today when _selectedDate is null in week view', () => {
// Arrange
component['_selectedDate'].set(null);
// Calculate expected date (today + 7 days)
const expectedDate = new Date();
expectedDate.setDate(expectedDate.getDate() + 7);
@ -287,6 +292,7 @@ describe('ScheduleComponent', () => {
// Assert
const newDate = component['_selectedDate']();
expect(newDate?.getDate()).toBe(expectedDate.getDate());
expect(newDate?.getHours()).toBe(0); // Normalized to midnight
});
it('should go to next month in month view', () => {

View file

@ -109,17 +109,8 @@ export class ScheduleComponent {
}
}
if (width < 600) {
return 3;
} else if (width < 900) {
return 4;
} else if (width < 1900) {
return 5;
} else if (width < 2200) {
return 7;
} else {
return 10;
}
// Week view: always 7 days
return 7;
});
daysToShow = computed(() => {
@ -220,12 +211,10 @@ export class ScheduleComponent {
);
this._selectedDate.set(previousMonth);
} else {
// Week view: subtract 7 days (create fresh Date to avoid mutations)
const previousWeek = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate() - 7,
);
// Week view: always subtract 7 days (full week)
const previousWeek = new Date(currentDate);
previousWeek.setDate(currentDate.getDate() - 7);
previousWeek.setHours(0, 0, 0, 0);
this._selectedDate.set(previousWeek);
}
}
@ -243,12 +232,10 @@ export class ScheduleComponent {
);
this._selectedDate.set(nextMonth);
} else {
// Week view: add 7 days (create fresh Date to avoid mutations)
const nextWeek = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate() + 7,
);
// Week view: always add 7 days (full week)
const nextWeek = new Date(currentDate);
nextWeek.setDate(currentDate.getDate() + 7);
nextWeek.setHours(0, 0, 0, 0);
this._selectedDate.set(nextWeek);
}
}