Merged PR 203: Improve responsive design and refactor layout

This commit is contained in:
Eric Nguyen 2022-10-03 12:05:16 +00:00
parent 50626218ba
commit 0d05f0959c
27 changed files with 968 additions and 485 deletions

View file

@ -0,0 +1,251 @@
import { describe, expect, it, vi } from 'vitest';
import * as React from 'react';
import { fireEvent, render, screen } from '../../utils/test-utils';
import { ElementsList } from './ElementsList';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { PositionReference } from '../../Enums/PositionReference';
import { FindContainerById } from '../../utils/itertools';
import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
import { Orientation } from '../../Enums/Orientation';
describe.concurrent('Elements sidebar', () => {
it('With a MainContainer', () => {
render(<ElementsList
symbols={new Map()}
mainContainer={{
children: [],
parent: null,
properties: DEFAULT_MAINCONTAINER_PROPS,
userData: {}
}}
selectedContainer={undefined}
onPropertyChange={() => {}}
selectContainer={() => {}}
addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
expect(screen.queryByText('id')).toBeNull();
expect(screen.getByText(/main/i));
});
it('With a selected MainContainer', () => {
const mainContainer: IContainerModel = {
children: [],
parent: null,
properties: DEFAULT_MAINCONTAINER_PROPS,
userData: {}
};
const { container } = render(<ElementsList
symbols={new Map()}
mainContainer={mainContainer}
selectedContainer={mainContainer}
onPropertyChange={() => {}}
selectContainer={() => {}}
addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
expect(screen.getByText(/main/i));
expect(screen.queryByText('id')).toBeDefined();
expect(screen.queryByText('parentId')).toBeDefined();
expect(screen.queryByText('x')).toBeDefined();
expect(screen.queryByText('y')).toBeDefined();
expect(screen.queryByText('width')).toBeDefined();
expect(screen.queryByText('height')).toBeDefined();
const propertyId = container.querySelector('#id');
const propertyParentId = container.querySelector('#parentId');
const propertyX = container.querySelector('#x');
const propertyY = container.querySelector('#y');
const propertyWidth = container.querySelector('#width');
const propertyHeight = container.querySelector('#height');
expect((propertyId as HTMLInputElement).value).toBe(mainContainer.properties.id.toString());
expect(propertyParentId).toBeDefined();
expect((propertyParentId as HTMLInputElement).value).toBe('');
expect(propertyX).toBeDefined();
expect((propertyX as HTMLInputElement).value).toBe(mainContainer.properties.x.toString());
expect(propertyY).toBeDefined();
expect((propertyY as HTMLInputElement).value).toBe(mainContainer.properties.y.toString());
expect(propertyWidth).toBeDefined();
expect((propertyWidth as HTMLInputElement).value).toBe(mainContainer.properties.width.toString());
expect(propertyHeight).toBeDefined();
expect((propertyHeight as HTMLInputElement).value).toBe(mainContainer.properties.height.toString());
});
it('With multiple containers', () => {
const children: IContainerModel[] = [];
const mainContainer: IContainerModel = {
children,
parent: null,
properties: DEFAULT_MAINCONTAINER_PROPS,
userData: {}
};
children.push(
{
children: [],
parent: mainContainer,
properties: {
id: 'child-1',
parentId: 'main',
linkedSymbolId: '',
displayedText: 'child-1',
orientation: Orientation.Horizontal,
x: 0,
y: 0,
minWidth: 1,
minHeight: 1,
width: 0,
height: 0,
margin: {},
isFlex: false,
maxWidth: Infinity,
maxHeight: Infinity,
type: 'type',
isAnchor: false,
warning: '',
hideChildrenInTreeview: false,
showChildrenDimensions: [],
showSelfDimensions: [],
showDimensionWithMarks: [],
markPosition: [],
positionReference: PositionReference.TopLeft
},
userData: {}
}
);
children.push(
{
children: [],
parent: mainContainer,
properties: {
id: 'child-2',
parentId: 'main',
linkedSymbolId: '',
displayedText: 'child-2',
orientation: Orientation.Horizontal,
x: 0,
y: 0,
margin: {},
minWidth: 1,
minHeight: 1,
width: 0,
height: 0,
positionReference: PositionReference.TopLeft,
isFlex: false,
maxWidth: Infinity,
maxHeight: Infinity,
type: 'type',
warning: '',
hideChildrenInTreeview: false,
showChildrenDimensions: [],
showSelfDimensions: [],
showDimensionWithMarks: [],
markPosition: [],
isAnchor: false
},
userData: {}
}
);
render(<ElementsList
symbols={new Map()}
mainContainer={mainContainer}
selectedContainer={mainContainer}
onPropertyChange={() => {}}
selectContainer={() => {}}
addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
expect(screen.queryByText('id')).toBeDefined();
expect(screen.getByText(/main/i));
expect(screen.getByText(/child-1/i));
expect(screen.getByText(/child-2/i));
});
it('With multiple containers, change selection', () => {
const children: IContainerModel[] = [];
const mainContainer: IContainerModel = {
children,
parent: null,
properties: DEFAULT_MAINCONTAINER_PROPS,
userData: {}
};
const child1Model: IContainerModel = {
children: [],
parent: mainContainer,
properties: {
id: 'child-1',
parentId: 'main',
linkedSymbolId: '',
displayedText: 'child-1',
orientation: Orientation.Horizontal,
x: 0,
y: 0,
minWidth: 1,
minHeight: 1,
width: 0,
height: 0,
warning: '',
positionReference: PositionReference.TopLeft,
margin: {},
isFlex: false,
maxWidth: Infinity,
maxHeight: Infinity,
type: 'type',
hideChildrenInTreeview: false,
showChildrenDimensions: [],
showSelfDimensions: [],
showDimensionWithMarks: [],
markPosition: [],
isAnchor: false
},
userData: {}
};
children.push(child1Model);
let selectedContainer: IContainerModel | undefined = mainContainer;
const selectContainer = vi.fn((containerId: string) => {
selectedContainer = FindContainerById(mainContainer, containerId);
});
const { container, rerender } = render(<ElementsList
symbols={new Map()}
mainContainer={mainContainer}
selectedContainer={selectedContainer}
onPropertyChange={() => {}}
selectContainer={selectContainer}
addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
expect(screen.queryByText('id')).toBeDefined();
expect(screen.getByText(/main/i));
const child1 = screen.getByText(/child-1/i);
expect(child1);
const propertyId = container.querySelector('#id');
const propertyParentId = container.querySelector('#parentId');
expect((propertyId as HTMLInputElement).value).toBe(mainContainer.properties.id.toString());
expect((propertyParentId as HTMLInputElement).value).toBe('');
fireEvent.click(child1);
rerender(<ElementsList
symbols={new Map()}
mainContainer={mainContainer}
selectedContainer={selectedContainer}
onPropertyChange={() => {}}
selectContainer={selectContainer}
addContainer={() => {}}
/>);
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
expect((propertyParentId as HTMLInputElement).value === '').toBeFalsy();
expect((propertyId as HTMLInputElement).value).toBe(child1Model.properties.id.toString());
expect((propertyParentId as HTMLInputElement).value).toBe(child1Model.properties.parentId?.toString());
});
});

View file

@ -0,0 +1,187 @@
import * as React from 'react';
import { FixedSizeList as List } from 'react-window';
import { Properties } from '../ContainerProperties/ContainerProperties';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { PropertyType } from '../../Enums/PropertyType';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
interface IElementsListProps {
mainContainer: IContainerModel
symbols: Map<string, ISymbolModel>
selectedContainer: IContainerModel | undefined
onPropertyChange: (
key: string,
value: string | number | boolean | number[],
type?: PropertyType
) => void
selectContainer: (containerId: string) => void
addContainer: (index: number, type: string, parent: string) => void
}
function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void {
const bordersClasses = ['border-t-8', 'border-8', 'border-b-8'].filter(className => className !== exception);
target.classList.remove(...bordersClasses);
}
function HandleDragLeave(event: React.DragEvent): void {
const target: HTMLButtonElement = event.target as HTMLButtonElement;
RemoveBorderClasses(target);
}
function HandleDragOver(
event: React.DragEvent,
mainContainer: IContainerModel
): void {
event.preventDefault();
const target: HTMLButtonElement = event.target as HTMLButtonElement;
const rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element.
if (target.id === mainContainer.properties.id) {
target.classList.add('border-8');
return;
}
if (y < 12) {
RemoveBorderClasses(target, 'border-t-8');
target.classList.add('border-t-8');
} else if (y < 24) {
RemoveBorderClasses(target, 'border-8');
target.classList.add('border-8');
} else {
RemoveBorderClasses(target, 'border-b-8');
target.classList.add('border-b-8');
}
}
function HandleOnDrop(
event: React.DragEvent,
mainContainer: IContainerModel,
addContainer: (index: number, type: string, parent: string) => void
): void {
event.preventDefault();
const type = event.dataTransfer.getData('type');
const target: HTMLButtonElement = event.target as HTMLButtonElement;
RemoveBorderClasses(target);
const targetContainer: IContainerModel | undefined = FindContainerById(
mainContainer,
target.id
);
if (targetContainer === undefined) {
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
}
if (targetContainer === mainContainer) {
// if the container is the root, only add type as child
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
return;
}
if (targetContainer.parent === null ||
targetContainer.parent === undefined) {
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
}
const rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element.
// locate the hitboxes
if (y < 12) {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index,
type,
targetContainer.parent.properties.id
);
} else if (y < 24) {
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
} else {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index + 1,
type,
targetContainer.parent.properties.id
);
}
}
export function ElementsList(props: IElementsListProps): JSX.Element {
// Render
const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true);
const containers = [...it];
function Row({
index, style
}: {
index: number
style: React.CSSProperties
}): JSX.Element {
const { container, depth } = containers[index];
const key = container.properties.id.toString();
const tabs = '|\t'.repeat(depth);
const text = container.properties.displayedText === key
? `${key}`
: `${container.properties.displayedText}`;
const isSelected = props.selectedContainer !== undefined &&
props.selectedContainer !== null &&
props.selectedContainer.properties.id === container.properties.id;
const selectedClass: string = isSelected
? 'border-l-4 bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50'
: 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400';
return (
<button type="button"
className={`transition-all w-full border-blue-500 hover:shadow-lg elements-sidebar-row whitespace-pre
text-left text-sm font-medium inline-flex ${container.properties.type} ${selectedClass}`}
id={key}
key={key}
style={style}
title={container.properties.warning}
onClick={() => props.selectContainer(container.properties.id)}
onDrop={(event) => HandleOnDrop(event, props.mainContainer, props.addContainer)}
onDragOver={(event) => HandleDragOver(event, props.mainContainer)}
onDragLeave={(event) => HandleDragLeave(event)}
>
{tabs}
{text}
{container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='w-8'/>
}
</button>
);
}
return (
<div className='h-full flex flex-col'>
<div className='h-48'>
<List
className="List divide-y divide-black overflow-y-auto"
itemCount={containers.length}
itemSize={35}
height={192}
width={'100%'}
>
{Row}
</List>
</div>
<div className='grow overflow-auto'>
<Properties
properties={props.selectedContainer?.properties}
symbols={props.symbols}
onChange={props.onPropertyChange}
/>
</div>
</div>
);
}