feat: made List more extensible

This commit is contained in:
doprz
2024-03-06 10:46:47 -06:00
parent b800c58502
commit cd34601379
6 changed files with 153 additions and 82 deletions

View File

@@ -29,8 +29,8 @@
"react-devtools-core": "^5.0.0", "react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"sass": "^1.69.5", "sass": "^1.70.0",
"sql.js": "1.9.0", "sql.js": "1.10.2",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {

16
pnpm-lock.yaml generated
View File

@@ -46,6 +46,9 @@ dependencies:
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-window:
specifier: ^1.8.10
version: 1.8.10(react-dom@18.2.0)(react@18.2.0)
sass: sass:
specifier: ^1.70.0 specifier: ^1.70.0
version: 1.70.0 version: 1.70.0
@@ -10756,6 +10759,19 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: true dev: true
/react-window@1.8.10(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.23.9
memoize-one: 5.2.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react@18.2.0: /react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View File

@@ -1,43 +1,76 @@
import React from 'react'; import React from 'react';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { Course, Status } from 'src/shared/types/Course'; import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting'; import { CourseMeeting } from '@shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule'; import Instructor from '@shared/types/Instructor';
import Instructor from 'src/shared/types/Instructor'; import List from '@views/components/common/List/List';
import List from 'src/views/components/common/List/List'; import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import CalendarCourseMeeting from 'src/views/components/common/CalendarCourseBlock/CalendarCourseMeeting'; import { getCourseColors } from 'src/shared/util/colors';
import { test_colors } from './PopupCourseBlock.stories';
const placeholderCourse: Course = new Course({
uniqueId: 123, const numberOfCourses = 5;
number: '311C',
fullName: "311C - Bevo's Default Course", const generateCourses = (count) => {
courseName: "Bevo's Default Course", const courses = [];
department: 'BVO',
for (let i = 0; i < count; i++) {
const course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3, creditHours: 3,
status: Status.OPEN, department: 'C S',
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })], description: [
isReserved: false, 'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
url: '', 'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
flags: [], 'May be counted toward the Quantitative Reasoning flag requirement.',
schedule: new CourseSchedule({ 'Designed to accommodate 100 or more students.',
meetings: [ 'Taught as a Web-based course.',
new CourseMeeting({ ],
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F], flags: ['Quantitative Reasoning'],
startTime: 480, fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
endTime: 570, instructionMode: 'Online',
location: { instructors: [
building: 'UTC', new Instructor({
room: '123', firstName: 'Bevo',
}, lastName: 'Bevo',
fullName: 'Bevo Bevo',
}), }),
], ],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}), }),
instructionMode: 'In Person', ],
semester: {
year: 2024,
season: 'Spring',
}, },
}); semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 12345 + i, // Make uniqueId different for each course
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
courses.push(course);
}
return courses;
};
const exampleCourses = generateCourses(numberOfCourses);
const generateCourseBlocks = (exampleCourses, colors) => {
return exampleCourses.map((course, i) => (
<PopupCourseBlock key={course.uniqueId} course={course} colors={colors[i]} />
))
}
const exampleCourseBlocks = generateCourseBlocks(exampleCourses, test_colors);
const meta = { const meta = {
title: 'Components/Common/List', title: 'Components/Common/List',
@@ -48,6 +81,10 @@ const meta = {
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
draggableElements: { control: 'object' }, draggableElements: { control: 'object' },
itemHeight: { control: 'number' },
listHeight: { control: 'number' },
listWidth: { control: 'number' },
gap: { control: 'number' },
}, },
} satisfies Meta<typeof List>; } satisfies Meta<typeof List>;
export default meta; export default meta;
@@ -56,12 +93,10 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: { args: {
draggableElements: [ draggableElements: exampleCourseBlocks,
<CalendarCourseMeeting key="1" course={placeholderCourse} meetingIdx={0} color="lightblue" />, itemHeight: 55,
<CalendarCourseMeeting key="2" course={placeholderCourse} meetingIdx={0} color="lightgreen" />, listHeight: 300,
<CalendarCourseMeeting key="3" course={placeholderCourse} meetingIdx={0} color="lightcoral" />, listWidth: 300,
<CalendarCourseMeeting key="4" course={placeholderCourse} meetingIdx={0} color="lightgoldenrodyellow" />, gap: 8,
<CalendarCourseMeeting key="5" course={placeholderCourse} meetingIdx={0} color="lightpink" />
],
}, },
}; };

View File

@@ -95,7 +95,7 @@ export const Variants: Story = {
), ),
}; };
const colors = Object.keys(theme.colors) export const test_colors = Object.keys(theme.colors)
// check that the color is a colorway (is an object) // check that the color is a colorway (is an object)
.filter(color => typeof theme.colors[color] === 'object') .filter(color => typeof theme.colors[color] === 'object')
.slice(0, 17) .slice(0, 17)
@@ -104,7 +104,7 @@ const colors = Object.keys(theme.colors)
export const AllColors: Story = { export const AllColors: Story = {
render: props => ( render: props => (
<div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'> <div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'>
{colors.map((color, i) => ( {test_colors.map((color, i) => (
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} /> <PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
))} ))}
</div> </div>

View File

@@ -1,8 +1,12 @@
import React from 'react'; import React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
import { FixedSizeList, areEqual } from "react-window"; import { FixedSizeList, areEqual } from 'react-window';
import { ReactElement } from 'react';
/*Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
*
*/
/** /**
* Props for the List component. * Props for the List component.
@@ -10,48 +14,45 @@ import { FixedSizeList, areEqual } from "react-window";
export interface ListProps { export interface ListProps {
draggableElements: any[]; //Will later define draggableElements based on what types draggableElements: any[]; //Will later define draggableElements based on what types
//of components are draggable. //of components are draggable.
itemHeight: number;
listHeight: number;
listWidth: number;
gap: number; //Impacts the spacing between items in the list
} }
function initial(draggableElements: any[] = []) { function initial(draggableElements: any[] = []) {
return draggableElements.map((element, index) => ({ return draggableElements.map((element, index) => ({
id: `id:${index}`, id: `id:${index}`,
content: element content: element as ReactElement,
})); }));
} }
function reorder(list, startIndex: number, endIndex: number) { function reorder(list: { id: string, content: ReactElement }[], startIndex: number, endIndex: number) {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed); result.splice(endIndex, 0, removed);
return result; return result;
} }
function getStyle({ provided, style, isDragging }) { function getStyle({ provided, style, isDragging, gap }) {
const combined = { const combined = {
...style, ...style,
...provided.draggableProps.style ...provided.draggableProps.style
}; };
const marginBottom = 8; return combined;
const withSpacing = {
...combined,
height: isDragging ? combined.height : combined.height - marginBottom,
marginBottom
};
return withSpacing;
//return style;
} }
function Item({ provided, item, style, isDragging }) { function Item({ provided, item, style, isDragging, gap }) {
return ( return (
<div <div
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
style={getStyle({ provided, style, isDragging })} style={getStyle({ provided, style, isDragging, gap })}
className={`item ${isDragging ? "is-dragging" : ""}`} className={`item ${isDragging ? "is-dragging" : ""}`}
> >
{item.content} {React.cloneElement(item.content, {dragHandleProps: provided.dragHandleProps})}
</div> </div>
); );
} }
@@ -59,15 +60,20 @@ function Item({ provided, item, style, isDragging }) {
interface RowProps { interface RowProps {
data: any, //DraggableElements[]; Need to define DraggableElements interface once those components are ready data: any, //DraggableElements[]; Need to define DraggableElements interface once those components are ready
index: number, index: number,
style: any style: React.CSSProperties,
} }
const Row: React.FC<RowProps> = React.memo(({ data: items, index, style }) => { const Row: React.FC<RowProps> = React.memo(({ data: { items, gap }, index, style }) => {
const item = items[index]; const item = items[index];
const adjustedStyle = {
...style,
height: `calc(${style.height}px - ${gap}px)`, // Reduce the height by gap to accommodate the margin
marginBottom: `${gap}px` // Add gap as bottom margin
};
return ( return (
<Draggable draggableId={item.id} index={index} key={item.id}> <Draggable draggableId={item.id} index={index} key={item.id}>
{/*@ts-ignore*/} {/*@ts-ignore*/}
{provided => <Item provided={provided} item={item} style={style} />} {provided => <Item provided={provided} item={item} style={adjustedStyle} gap={gap}/>}
</Draggable> </Draggable>
); );
}, areEqual); }, areEqual);
@@ -78,15 +84,14 @@ const Row: React.FC<RowProps> = React.memo(({ data: items, index, style }) => {
* @example * @example
* <List draggableElements={elements} /> * <List draggableElements={elements} />
*/ */
const List: React.FC<ListProps> = ({ const List: React.FC<ListProps> = ( { draggableElements, itemHeight, listHeight, listWidth, gap=8 }: ListProps) => {
draggableElements
}: ListProps) => {
const [items, setItems] = useState(() => initial(draggableElements)); const [items, setItems] = useState(() => initial(draggableElements));
function onDragEnd(result) { function onDragEnd(result) {
if (!result.destination) { if (!result.destination) {
return; return;
} }
if (result.source.index === result.destination.index) { if (result.source.index === result.destination.index) {
return; return;
} }
@@ -96,10 +101,11 @@ const List: React.FC<ListProps> = ({
result.source.index, result.source.index,
result.destination.index result.destination.index
); );
setItems(newItems as {id: string, content: any}[]); setItems(newItems as {id: string, content: ReactElement}[]);
} }
return ( return (
<div style={{ overflow: 'hidden', width: listWidth, height: listHeight }}>
<DragDropContext onDragEnd = {onDragEnd}> <DragDropContext onDragEnd = {onDragEnd}>
<div style = {{ <div style = {{
display: "flex", display: "flex",
@@ -121,10 +127,20 @@ const List: React.FC<ListProps> = ({
.split(',') .split(',')
.map((v) => parseInt(v, 10)); .map((v) => parseInt(v, 10));
style.transform = `translate(0px, ${y}px)`; const minTranslateY = -1 * rubric.source.index * itemHeight;
const maxTranslateY = (items.length - rubric.source.index - 1) * itemHeight;
if (y < minTranslateY) {
}
else if (y > maxTranslateY) {
}
else {
} }
style.fontFamily = "Inter"; style.transform = `translate(0px, ${y}px)`; // Apply constrained y value
}
return ( return (
<Item <Item
@@ -134,18 +150,19 @@ const List: React.FC<ListProps> = ({
style = {{ style = {{
style style
}} }}
gap = {gap}
/> />
)} )}
} }
> >
{provided => ( {provided => (
<FixedSizeList <FixedSizeList
height={500} height={listHeight}
itemCount={items.length} itemCount={items.length}
itemSize={80} itemSize={itemHeight}
width={300} width={listWidth}
outerRef={provided.innerRef} outerRef={provided.innerRef}
itemData={items} itemData={{ items, gap }}
> >
{Row} {Row}
</FixedSizeList> </FixedSizeList>
@@ -153,6 +170,7 @@ const List: React.FC<ListProps> = ({
</Droppable> </Droppable>
</div> </div>
</DragDropContext> </DragDropContext>
</div>
); );
}; };

View File

@@ -13,6 +13,7 @@ export interface PopupCourseBlockProps {
className?: string; className?: string;
course: Course; course: Course;
colors: CourseColors; colors: CourseColors;
dragHandleProps?: any;
} }
/** /**
@@ -20,7 +21,7 @@ export interface PopupCourseBlockProps {
* *
* @param props PopupCourseBlockProps * @param props PopupCourseBlockProps
*/ */
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element { export default function PopupCourseBlock({ className, course, colors, dragHandleProps }: PopupCourseBlockProps): JSX.Element {
// whiteText based on secondaryColor // whiteText based on secondaryColor
const fontColor = pickFontColor(colors.primaryColor); const fontColor = pickFontColor(colors.primaryColor);
@@ -36,6 +37,7 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
backgroundColor: colors.secondaryColor, backgroundColor: colors.secondaryColor,
}} }}
className='flex cursor-move items-center self-stretch rounded rounded-r-0' className='flex cursor-move items-center self-stretch rounded rounded-r-0'
{...dragHandleProps}
> >
<DragIndicatorIcon className='h-6 w-6 text-white' /> <DragIndicatorIcon className='h-6 w-6 text-white' />
</div> </div>