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',
|
// 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
|
<CalendarHeader
|
||||||
sidebarOpen={showSidebar}
|
sidebarOpen={showSidebar}
|
||||||
@@ -148,7 +148,7 @@ export default function Calendar(): JSX.Element {
|
|||||||
setShowSidebar(!showSidebar);
|
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} />
|
<CalendarGrid courseCells={courseCells} setCourse={setCourse} />
|
||||||
</div>
|
</div>
|
||||||
<CalendarBottomBar courseCells={courseCells} setCourse={setCourse} />
|
<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 Text from '@views/components/common/Text/Text';
|
||||||
import { ColorPickerProvider } from '@views/contexts/ColorPickerContext';
|
import { ColorPickerProvider } from '@views/contexts/ColorPickerContext';
|
||||||
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import CalendarCell from './CalendarGridCell';
|
import CalendarCell from './CalendarGridCell';
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ function makeGridRow(row: number, cols: number): JSX.Element {
|
|||||||
const hour = hoursOfDay[row]!;
|
const hour = hoursOfDay[row]!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={row}>
|
||||||
<CalendarHour hour={hour} />
|
<CalendarHour hour={hour} />
|
||||||
<div className='grid-row-span-2 w-4 border-b border-r border-gray-300' />
|
<div className='grid-row-span-2 w-4 border-b border-r border-gray-300' />
|
||||||
{[...Array(cols).keys()].map(col => (
|
{[...Array(cols).keys()].map(col => (
|
||||||
<CalendarCell key={`${row}${col}`} row={row} col={col} />
|
<CalendarCell key={`${row}${col}`} row={row} col={col} />
|
||||||
))}
|
))}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,23 +56,40 @@ export default function CalendarGrid({
|
|||||||
setCourse,
|
setCourse,
|
||||||
}: React.PropsWithChildren<Props>): JSX.Element {
|
}: React.PropsWithChildren<Props>): JSX.Element {
|
||||||
return (
|
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 */}
|
{/* Displaying day labels */}
|
||||||
<div />
|
|
||||||
<div className='w-4 border-b border-r border-gray-300' />
|
|
||||||
{daysOfWeek.map(day => (
|
{daysOfWeek.map(day => (
|
||||||
<div className='h-4 flex items-end justify-center border-b border-r border-gray-300 pb-1.5'>
|
<div
|
||||||
<Text key={day} variant='small' className='text-center text-ut-burntorange' as='div'>
|
// Full height with background to prevent grid lines from showing behind
|
||||||
{day}
|
className='sticky top-[85px] z-10 row-span-2 h-7 flex flex-col items-end self-start justify-end bg-white'
|
||||||
</Text>
|
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>
|
</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))}
|
{[...Array(13).keys()].map(i => makeGridRow(i, 5))}
|
||||||
<CalendarHour hour={21} />
|
<CalendarHour hour={21} />
|
||||||
{Array(6)
|
{Array(6)
|
||||||
.fill(1)
|
.fill(1)
|
||||||
.map(() => (
|
.map((_, i) => (
|
||||||
<div className='h-4 flex items-end justify-center border-r border-gray-300' />
|
// 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>
|
<ColorPickerProvider>
|
||||||
{courseCells && <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} />}
|
{courseCells && <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} />}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ function CalendarCell({ row, col }: Props): JSX.Element {
|
|||||||
<div
|
<div
|
||||||
className='h-full w-full flex items-center border-b border-r border-gray-300'
|
className='h-full w-full flex items-center border-b border-r border-gray-300'
|
||||||
style={{
|
style={{
|
||||||
gridColumn: col + 3,
|
gridColumn: col + 3, // start in the 3rd 1-index column
|
||||||
gridRow: `${2 * row + 2} / ${2 * row + 4}`,
|
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' />
|
<div className='h-0 w-full border-t border-gray-300/25' />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ scrollbarGutter: 'stable' }}
|
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 && (
|
{!sidebarOpen && (
|
||||||
<Button
|
<Button
|
||||||
@@ -62,7 +62,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
|||||||
className={clsx([
|
className={clsx([
|
||||||
styleResetClass,
|
styleResetClass,
|
||||||
'mt-spacing-3',
|
'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-[closed]:(opacity-0 scale-95)',
|
||||||
'data-[enter]:(ease-out-expo duration-150)',
|
'data-[enter]:(ease-out-expo duration-150)',
|
||||||
'data-[leave]:(ease-out duration-50)',
|
'data-[leave]:(ease-out duration-50)',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { convertMinutesToIndex } from '../useFlattenedCourseSchedule';
|
|||||||
describe('useFlattenedCourseSchedule', () => {
|
describe('useFlattenedCourseSchedule', () => {
|
||||||
it('should convert minutes to index correctly', () => {
|
it('should convert minutes to index correctly', () => {
|
||||||
const minutes = 480; // 8:00 AM
|
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);
|
const result = convertMinutesToIndex(minutes);
|
||||||
expect(result).toBe(expectedIndex);
|
expect(result).toBe(expectedIndex);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ export interface FlattenedCourseSchedule {
|
|||||||
* @param minutes - The number of minutes.
|
* @param minutes - The number of minutes.
|
||||||
* @returns The index value.
|
* @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.
|
* Get the active schedule, and convert it to be render-able into a calendar.
|
||||||
|
|||||||
Reference in New Issue
Block a user