feat: sticky calendar header and days (#568)
* feat: sticky calendar days * feat: partial height borders for day labels * feat: make calendar header actually sticky * fix: remove unneeded gap * refactor: add preston as co-author Co-authored-by: Preston-Cook <preston.l.cook@gmail.com> * fix: z-index issues with export sub-buttons --------- Co-authored-by: Preston-Cook <preston.l.cook@gmail.com> Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
@@ -140,7 +140,7 @@ export default function Calendar(): JSX.Element {
|
||||
// scrollbarGutter: 'stable',
|
||||
}
|
||||
}
|
||||
className='h-full flex flex-grow flex-col overflow-x-scroll px-spacing-5'
|
||||
className='z-1 h-full flex flex-grow flex-col overflow-x-scroll [&>*]:px-spacing-5'
|
||||
>
|
||||
<CalendarHeader
|
||||
sidebarOpen={showSidebar}
|
||||
@@ -148,7 +148,7 @@ export default function Calendar(): JSX.Element {
|
||||
setShowSidebar(!showSidebar);
|
||||
}}
|
||||
/>
|
||||
<div className='min-h-2xl min-w-5xl flex-grow overflow-auto pl-spacing-3 pt-spacing-3 screenshot:min-h-xl'>
|
||||
<div className='min-h-2xl min-w-5xl flex-grow gap-0 pl-spacing-3 screenshot:min-h-xl'>
|
||||
<CalendarGrid courseCells={courseCells} setCourse={setCourse} />
|
||||
</div>
|
||||
<CalendarBottomBar courseCells={courseCells} setCourse={setCourse} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import { ColorPickerProvider } from '@views/contexts/ColorPickerContext';
|
||||
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import CalendarCell from './CalendarGridCell';
|
||||
|
||||
@@ -30,13 +30,13 @@ function makeGridRow(row: number, cols: number): JSX.Element {
|
||||
const hour = hoursOfDay[row]!;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Fragment key={row}>
|
||||
<CalendarHour hour={hour} />
|
||||
<div className='grid-row-span-2 w-4 border-b border-r border-gray-300' />
|
||||
{[...Array(cols).keys()].map(col => (
|
||||
<CalendarCell key={`${row}${col}`} row={row} col={col} />
|
||||
))}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,23 +56,40 @@ export default function CalendarGrid({
|
||||
setCourse,
|
||||
}: React.PropsWithChildren<Props>): JSX.Element {
|
||||
return (
|
||||
<div className='grid grid-cols-[auto_auto_repeat(5,1fr)] grid-rows-[auto_repeat(26,1fr)] h-full'>
|
||||
<div className='grid grid-cols-[auto_auto_repeat(5,1fr)] grid-rows-[auto_auto_repeat(27,1fr)] h-full'>
|
||||
{/* Cover top left corner of grid, so time gets cut off at the top of the partial border */}
|
||||
<div className='sticky top-[85px] z-10 col-span-2 h-3 bg-white' />
|
||||
{/* Displaying day labels */}
|
||||
<div />
|
||||
<div className='w-4 border-b border-r border-gray-300' />
|
||||
{daysOfWeek.map(day => (
|
||||
<div className='h-4 flex items-end justify-center border-b border-r border-gray-300 pb-1.5'>
|
||||
<Text key={day} variant='small' className='text-center text-ut-burntorange' as='div'>
|
||||
<div
|
||||
// Full height with background to prevent grid lines from showing behind
|
||||
className='sticky top-[85px] z-10 row-span-2 h-7 flex flex-col items-end self-start justify-end bg-white'
|
||||
key={day}
|
||||
>
|
||||
{/* Partial border height because that's what Isaiah wants */}
|
||||
<div className='h-4 w-full flex items-end border-b border-r border-gray-300'>
|
||||
{/* Alignment for text */}
|
||||
<div className='h-[calc(1.75rem_-_1px)] w-full flex items-center justify-center'>
|
||||
<Text variant='small' className='text-center text-ut-burntorange' as='div'>
|
||||
{day}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* empty slot, for alignment */}
|
||||
<div />
|
||||
{/* time tick for the first hour */}
|
||||
<div className='h-4 w-4 self-end border-b border-r border-gray-300' />
|
||||
{[...Array(13).keys()].map(i => makeGridRow(i, 5))}
|
||||
<CalendarHour hour={21} />
|
||||
{Array(6)
|
||||
.fill(1)
|
||||
.map(() => (
|
||||
<div className='h-4 flex items-end justify-center border-r border-gray-300' />
|
||||
.map((_, i) => (
|
||||
// Key suppresses warning about duplicate keys,
|
||||
// and index is fine because it doesn't change between renders
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={i} className='h-4 flex items-end justify-center border-r border-gray-300' />
|
||||
))}
|
||||
<ColorPickerProvider>
|
||||
{courseCells && <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} />}
|
||||
|
||||
@@ -17,8 +17,8 @@ function CalendarCell({ row, col }: Props): JSX.Element {
|
||||
<div
|
||||
className='h-full w-full flex items-center border-b border-r border-gray-300'
|
||||
style={{
|
||||
gridColumn: col + 3,
|
||||
gridRow: `${2 * row + 2} / ${2 * row + 4}`,
|
||||
gridColumn: col + 3, // start in the 3rd 1-index column
|
||||
gridRow: `${2 * row + 3} / ${2 * row + 5}`, // Span 2 rows, skip 2 header rows
|
||||
}}
|
||||
>
|
||||
<div className='h-0 w-full border-t border-gray-300/25' />
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
||||
return (
|
||||
<div
|
||||
style={{ scrollbarGutter: 'stable' }}
|
||||
className='sticky left-0 right-0 min-h-[85px] flex items-center gap-5 overflow-x-scroll overflow-y-hidden pl-spacing-7 pt-spacing-5'
|
||||
className='sticky left-0 right-0 top-0 z-10 min-h-[85px] flex items-center gap-5 overflow-x-scroll overflow-y-hidden bg-white pl-spacing-7 pt-spacing-5'
|
||||
>
|
||||
{!sidebarOpen && (
|
||||
<Button
|
||||
@@ -62,7 +62,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
||||
className={clsx([
|
||||
styleResetClass,
|
||||
'mt-spacing-3',
|
||||
'min-w-max cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite/50 focus:outline-none',
|
||||
'min-w-max cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite/50 focus:outline-none z-20',
|
||||
'data-[closed]:(opacity-0 scale-95)',
|
||||
'data-[enter]:(ease-out-expo duration-150)',
|
||||
'data-[leave]:(ease-out duration-50)',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { convertMinutesToIndex } from '../useFlattenedCourseSchedule';
|
||||
describe('useFlattenedCourseSchedule', () => {
|
||||
it('should convert minutes to index correctly', () => {
|
||||
const minutes = 480; // 8:00 AM
|
||||
const expectedIndex = 2; // (480 - 420) / 30 = 2
|
||||
const expectedIndex = 3; // (480 - 480) / 30 + 2 + 1 = 3
|
||||
const result = convertMinutesToIndex(minutes);
|
||||
expect(result).toBe(expectedIndex);
|
||||
});
|
||||
|
||||
@@ -52,7 +52,9 @@ export interface FlattenedCourseSchedule {
|
||||
* @param minutes - The number of minutes.
|
||||
* @returns The index value.
|
||||
*/
|
||||
export const convertMinutesToIndex = (minutes: number): number => Math.floor((minutes - 420) / 30);
|
||||
export const convertMinutesToIndex = (minutes: number): number =>
|
||||
// 480 = 8 a.m., 30 = 30 minute slots, 2 header rows, and grid rows start at 1
|
||||
Math.floor((minutes - 480) / 30) + 2 + 1;
|
||||
|
||||
/**
|
||||
* Get the active schedule, and convert it to be render-able into a calendar.
|
||||
|
||||
Reference in New Issue
Block a user