feat: update dialog component to headlessui (#159)

This commit is contained in:
Razboy20
2024-03-13 23:09:43 -05:00
committed by GitHub
parent df7a7c65d6
commit 442be8cbee
8 changed files with 149 additions and 170 deletions

View File

@@ -3,6 +3,7 @@ import { Status } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule'; import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup'; import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import React, { useState } from 'react';
import { bevoCourse, bevoScheule, MikeScottCS314Course, MikeScottCS314Schedule } from './mocked'; import { bevoCourse, bevoScheule, MikeScottCS314Course, MikeScottCS314Schedule } from './mocked';
@@ -10,6 +11,7 @@ const meta = {
title: 'Components/Injected/CourseCatalogInjectedPopup', title: 'Components/Injected/CourseCatalogInjectedPopup',
component: CourseCatalogInjectedPopup, component: CourseCatalogInjectedPopup,
args: { args: {
open: true,
onClose: () => {}, onClose: () => {},
}, },
argTypes: { argTypes: {
@@ -29,6 +31,12 @@ const meta = {
}, },
}, },
}, },
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isOpen, setIsOpen] = useState(args.open);
return <CourseCatalogInjectedPopup {...args} open={isOpen} onClose={() => setIsOpen(false)} />;
},
} satisfies Meta<typeof CourseCatalogInjectedPopup>; } satisfies Meta<typeof CourseCatalogInjectedPopup>;
export default meta; export default meta;

View File

@@ -24,11 +24,18 @@ interface Props {
export default function CourseCatalogMain({ support }: Props): JSX.Element { export default function CourseCatalogMain({ support }: Props): JSX.Element {
const [rows, setRows] = React.useState<ScrapedRow[]>([]); const [rows, setRows] = React.useState<ScrapedRow[]>([]);
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null); const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
const [showPopup, setShowPopup] = useState(false);
useEffect(() => { useEffect(() => {
populateSearchInputs(); populateSearchInputs();
}, []); }, []);
useEffect(() => {
if (selectedCourse) {
setShowPopup(true);
}
}, [selectedCourse]);
useEffect(() => { useEffect(() => {
const tableRows = getCourseTableRows(document); const tableRows = getCourseTableRows(document);
const ccs = new CourseCatalogScraper(support); const ccs = new CourseCatalogScraper(support);
@@ -75,13 +82,13 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element {
/> />
) )
)} )}
{selectedCourse && ( <CourseCatalogInjectedPopup
<CourseCatalogInjectedPopup course={selectedCourse}
course={selectedCourse} activeSchedule={activeSchedule}
activeSchedule={activeSchedule} show={showPopup}
onClose={handleClearSelectedCourse} onClose={() => setShowPopup(false)}
/> afterLeave={handleClearSelectedCourse}
)} />
<AutoLoad addRows={addRows} /> <AutoLoad addRows={addRows} />
</ExtensionRoot> </ExtensionRoot>
); );

View File

@@ -4,7 +4,6 @@ import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
import CalendarHeader from '@views/components/calendar/CalendarHeader/CalenderHeader'; import CalendarHeader from '@views/components/calendar/CalendarHeader/CalenderHeader';
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules'; import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
import ImportantLinks from '@views/components/calendar/ImportantLinks'; import ImportantLinks from '@views/components/calendar/ImportantLinks';
import TeamLinks from '@views/components/calendar/TeamLinks';
import Divider from '@views/components/common/Divider/Divider'; import Divider from '@views/components/common/Divider/Divider';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup'; import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule'; import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
@@ -19,6 +18,7 @@ export default function Calendar(): JSX.Element {
const calendarRef = useRef<HTMLDivElement>(null); const calendarRef = useRef<HTMLDivElement>(null);
const { courseCells, activeSchedule } = useFlattenedCourseSchedule(); const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
const [course, setCourse] = useState<Course | null>(null); const [course, setCourse] = useState<Course | null>(null);
const [showPopup, setShowPopup] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState('20%'); const [sidebarWidth, setSidebarWidth] = useState('20%');
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
@@ -48,6 +48,10 @@ export default function Calendar(): JSX.Element {
return () => window.removeEventListener('resize', adjustLayout); return () => window.removeEventListener('resize', adjustLayout);
}, []); }, []);
useEffect(() => {
if (course) setShowPopup(true);
}, [course]);
const calendarContainerStyle = { const calendarContainerStyle = {
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: 'top left', transformOrigin: 'top left',
@@ -68,10 +72,6 @@ export default function Calendar(): JSX.Element {
<div className='mt-4'> <div className='mt-4'>
<ImportantLinks /> <ImportantLinks />
</div> </div>
<Divider orientation='horizontal' size='100%' />
<div className='mt-4'>
<TeamLinks />
</div>
</div> </div>
<div className='flex flex-grow flex-col' style={calendarContainerStyle} ref={calendarRef}> <div className='flex flex-grow flex-col' style={calendarContainerStyle} ref={calendarRef}>
<div className='flex-grow overflow-auto'> <div className='flex-grow overflow-auto'>
@@ -80,13 +80,14 @@ export default function Calendar(): JSX.Element {
<CalendarBottomBar calendarRef={calendarRef} /> <CalendarBottomBar calendarRef={calendarRef} />
</div> </div>
</div> </div>
{course ? (
<CourseCatalogInjectedPopup <CourseCatalogInjectedPopup
course={course} course={course}
activeSchedule={activeSchedule} activeSchedule={activeSchedule}
onClose={() => setCourse(null)} onClose={() => setShowPopup(false)}
/> open={showPopup}
) : null} afterLeave={() => setCourse(null)}
/>
</div> </div>
); );
} }

View File

@@ -8,6 +8,34 @@ type Props = {
className?: string; className?: string;
}; };
interface LinkItem {
text: string;
url: string;
}
const links: LinkItem[] = [
{
text: "Summer '24 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/',
},
{
text: "Fall '24 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20236/',
},
{
text: 'Registration Info Sheet',
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',
},
{
text: 'Register For Courses',
url: 'https://utdirect.utexas.edu/registration/chooseSemester.WBX',
},
{
text: 'Degree Audit',
url: 'https://utdirect.utexas.edu/apps/degree/audits/',
},
];
/** /**
* The "Important Links" section of the calendar website * The "Important Links" section of the calendar website
* @returns * @returns
@@ -15,52 +43,19 @@ type Props = {
export default function ImportantLinks({ className }: Props): JSX.Element { export default function ImportantLinks({ className }: Props): JSX.Element {
return ( return (
<article className={clsx(className, 'flex flex-col gap-2')}> <article className={clsx(className, 'flex flex-col gap-2')}>
<Text variant='h3'>Important Links</Text> <Text variant='h3'>Useful Links</Text>
<a {links.map((link, index) => (
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/' <a
className='flex items-center gap-0.5 text-ut-burntorange' key={link.text}
target='_blank' href={link.url}
rel='noreferrer' className='flex items-center gap-0.5 text-ut-burntorange'
> target='_blank'
<Text variant='p'>Spring Course Schedule</Text> rel='noreferrer'
<OutwardArrowIcon className='h-3 w-3' /> >
</a> <Text variant='p'>{link.text}</Text>
<a <OutwardArrowIcon className='h-3 w-3' />
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20236/' </a>
className='flex items-center gap-0.5 text-ut-burntorange' ))}
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Summer Course Schedule</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/registrar/ris.WBX'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Registration Info Sheet</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/registration/chooseSemester.WBX'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Register For Courses</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/apps/degree/audits/'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Degree Audit</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
</article> </article>
); );
} }

View File

@@ -0,0 +1,58 @@
import type { TransitionRootProps } from '@headlessui/react';
import { Dialog as HDialog, Transition } from '@headlessui/react';
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import React, { Fragment } from 'react';
export interface _DialogProps {
className?: string;
title?: JSX.Element;
description?: JSX.Element;
}
export type DialogProps = _DialogProps & Omit<TransitionRootProps<typeof HDialog>, 'children'>;
/**
* A reusable popup component that can be used to display content on the page
*/
export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Element {
const { children, className, open, onTransitionEnd, ...rest } = props;
return (
<Transition show={open} as={HDialog} {...rest}>
<Transition.Child
as={Fragment}
enter='transition duration-300 ease-out'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='transition duration-150 ease-in delay-25'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className={clsx('fixed inset-0 z-50 bg-neutral-500/25')} />
</Transition.Child>
<Transition.Child
as={Fragment}
enter='transition duration-400 ease-[cubic-bezier(0.15,0.3,0.2,1)]'
enterFrom='transform scale-95 opacity-0'
enterTo='transform scale-100 opacity-100'
leave='transition duration-250 ease-[cubic-bezier(0.23,0.01,0.92,0.72)]'
leaveFrom='transform scale-100 opacity-100'
leaveTo='transform scale-95 opacity-0'
>
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<HDialog.Panel
className={clsx(
'z-99 max-h-[80vh] flex flex-col overflow-y-scroll rounded bg-white shadow-xl',
className
)}
>
{props.title && <HDialog.Title>{props.title}</HDialog.Title>}
{props.description && <HDialog.Description>{props.description}</HDialog.Description>}
{children}
</HDialog.Panel>
</div>
</Transition.Child>
</Transition>
);
}

View File

@@ -1,24 +0,0 @@
@use 'src/views/styles/colors.module.scss';
.container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
&.overlay {
background-color: rgba(0, 0, 0, 0.5);
z-index: 2147483647;
}
}
.body {
overflow-y: auto;
z-index: 2147483647;
background-color: colors.$white;
box-shadow: 0px 12px 30px 0px #323e5f29;
transition: box-shadow 0.15s;
}

View File

@@ -1,65 +0,0 @@
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import React, { useCallback } from 'react';
import styles from './Popup.module.scss';
interface Props {
testId?: string;
style?: React.CSSProperties;
className?: string;
/** Should it display a subtle dark overlay over the rest of the screen */
overlay?: boolean;
onClose?: () => void;
}
/**
* A reusable popup component that can be used to display content on the page
*/
export default function Popup({
onClose,
children,
className,
style,
testId,
overlay,
}: PropsWithChildren<Props>): JSX.Element {
const containerRef = React.useRef<HTMLDivElement>(null);
const bodyRef = React.useRef<HTMLDivElement>(null);
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (!bodyRef.current) return;
if (!bodyRef.current.contains(event.target as Node)) {
onClose?.();
}
},
[onClose, bodyRef]
);
React.useEffect(() => {
const shadowRoot = document.getElementById('ut-registration-plus-container')?.shadowRoot;
if (!shadowRoot) return;
shadowRoot.addEventListener('mousedown', handleClickOutside);
return () => {
shadowRoot.removeEventListener('mousedown', handleClickOutside);
};
}, [handleClickOutside]);
return (
<div
style={style}
ref={containerRef}
className={clsx(styles.container, {
[styles.overlay]: overlay,
})}
data-testid={testId}
>
<div ref={bodyRef} className={clsx(styles.body, className)}>
{children}
</div>
</div>
);
}

View File

@@ -1,17 +1,17 @@
import type { Course } from '@shared/types/Course'; import type { Course } from '@shared/types/Course';
import type { UserSchedule } from '@shared/types/UserSchedule'; import type { UserSchedule } from '@shared/types/UserSchedule';
import Popup from '@views/components/common/Popup/Popup'; import type { DialogProps } from '@views/components/common/Dialog/Dialog';
import Dialog from '@views/components/common/Dialog/Dialog';
import React from 'react'; import React from 'react';
import Description from './Description'; import Description from './Description';
import GradeDistribution from './GradeDistribution'; import GradeDistribution from './GradeDistribution';
import HeadingAndActions from './HeadingAndActions'; import HeadingAndActions from './HeadingAndActions';
interface CourseCatalogInjectedPopupProps { export type CourseCatalogInjectedPopupProps = DialogProps & {
course: Course; course: Course;
activeSchedule: UserSchedule; activeSchedule: UserSchedule;
onClose: () => void; };
}
/** /**
* CourseCatalogInjectedPopup component displays a popup with course details. * CourseCatalogInjectedPopup component displays a popup with course details.
@@ -23,18 +23,17 @@ interface CourseCatalogInjectedPopupProps {
* @param {Function} props.onClose - The function to close the popup. * @param {Function} props.onClose - The function to close the popup.
* @returns {JSX.Element} The CourseCatalogInjectedPopup component. * @returns {JSX.Element} The CourseCatalogInjectedPopup component.
*/ */
export default function CourseCatalogInjectedPopup({ function CourseCatalogInjectedPopup({ course, activeSchedule, ...rest }: CourseCatalogInjectedPopupProps): JSX.Element {
course, const emptyRef = React.useRef<HTMLDivElement>(null);
activeSchedule,
onClose,
}: CourseCatalogInjectedPopupProps): JSX.Element {
return ( return (
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}> <Dialog className='max-w-[780px] px-6' {...rest} initialFocus={emptyRef}>
<div className='flex flex-col'> <div className='hidden' ref={emptyRef} />
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} /> <HeadingAndActions course={course} onClose={rest.onClose as () => void} activeSchedule={activeSchedule} />
<Description course={course} /> <Description course={course} />
<GradeDistribution course={course} /> <GradeDistribution course={course} />
</div> </Dialog>
</Popup>
); );
} }
export default React.memo(CourseCatalogInjectedPopup);