feat: made List more extensible
This commit is contained in:
@@ -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
16
pnpm-lock.yaml
generated
@@ -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'}
|
||||||
|
|||||||
@@ -1,44 +1,77 @@
|
|||||||
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',
|
||||||
component: List,
|
component: 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" />
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user