Skip to content

Commit

Permalink
feat(apps/phone): sortable grid
Browse files Browse the repository at this point in the history
  • Loading branch information
itschip committed Mar 2, 2024
1 parent 156e604 commit 6a4185c
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 32 deletions.
3 changes: 3 additions & 0 deletions apps/phone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"homepage": "/dist/html",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/css": "^11.10.5",
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.6.0",
Expand Down
282 changes: 282 additions & 0 deletions apps/phone/src/apps/home/Sortable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import {
Active,
Announcements,
closestCenter,
CollisionDetection,
DragOverlay,
DndContext,
DropAnimation,
KeyboardSensor,
KeyboardCoordinateGetter,
Modifiers,
MouseSensor,
MeasuringConfiguration,
PointerActivationConstraint,
ScreenReaderInstructions,
TouchSensor,
UniqueIdentifier,
useSensor,
useSensors,
defaultDropAnimationSideEffects,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
SortingStrategy,
rectSortingStrategy,
AnimateLayoutChanges,
NewIndexGetter,
useSortable,
} from '@dnd-kit/sortable';

import { useApps } from '@os/apps/hooks/useApps';
import { IApp } from '@os/apps/config/apps';
import { CSS } from '@dnd-kit/utilities';
import { AppIcon } from '@ui/components';

const defaultInitializer = (index: number) => index;

export function createRange<T = number>(
length: number,
initializer: (index: number) => any = defaultInitializer,
): T[] {
return [...new Array(length)].map((_, index) => initializer(index));
}

export interface Props {
activationConstraint?: PointerActivationConstraint;
animateLayoutChanges?: AnimateLayoutChanges;
adjustScale?: boolean;
collisionDetection?: CollisionDetection;
coordinateGetter?: KeyboardCoordinateGetter;
Container?: any; // To-do: Fix me
dropAnimation?: DropAnimation | null;
getNewIndex?: NewIndexGetter;
handle?: boolean;
itemCount?: number;
items?: UniqueIdentifier[];
measuring?: MeasuringConfiguration;
modifiers?: Modifiers;
renderItem?: any;
removable?: boolean;
reorderItems?: typeof arrayMove;
strategy?: SortingStrategy;
style?: React.CSSProperties;
useDragOverlay?: boolean;

getItemStyles?(args: {
id: UniqueIdentifier;
index: number;
isSorting: boolean;
isDragOverlay: boolean;
overIndex: number;
isDragging: boolean;
}): React.CSSProperties;

wrapperStyle?(args: {
active: Pick<Active, 'id'> | null;
index: number;
isDragging: boolean;
id: UniqueIdentifier;
}): React.CSSProperties;

isDisabled?(id: UniqueIdentifier): boolean;
}

const dropAnimationConfig: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.5',
},
},
}),
};

const screenReaderInstructions: ScreenReaderInstructions = {
draggable: `
To pick up a sortable item, press the space bar.
While sorting, use the arrow keys to move the item.
Press space again to drop the item in its new position, or press escape to cancel.
`,
};

export function SortableApps({
activationConstraint,
collisionDetection = closestCenter,
coordinateGetter = sortableKeyboardCoordinates,
itemCount = 16,
items: initialItems,
measuring,
modifiers,
removable,
reorderItems = arrayMove,
strategy = rectSortingStrategy,
}: Props) {
const [items, setItems] = useState<UniqueIdentifier[]>(
() => initialItems ?? createRange<UniqueIdentifier>(itemCount, (index) => index + 1),
);

const { apps, setApps } = useApps();

const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint,
}),
useSensor(TouchSensor, {
activationConstraint,
}),
useSensor(KeyboardSensor, {
// Disable smooth scrolling in Cypress automated tests
scrollBehavior: 'Cypress' in window ? 'auto' : undefined,
coordinateGetter,
}),
);
const isFirstAnnouncement = useRef(true);
const getIndex = (id: UniqueIdentifier) => items.indexOf(id);
const getPosition = (id: UniqueIdentifier) => getIndex(id) + 1;
const activeIndex = activeId ? getIndex(activeId) : -1;
const handleRemove = removable
? (id: UniqueIdentifier) => setItems((items) => items.filter((item) => item !== id))
: undefined;
const announcements: Announcements = {
onDragStart({ active: { id } }) {
return `Picked up sortable item ${String(
id,
)}. Sortable item ${id} is in position ${getPosition(id)} of ${items.length}`;
},
onDragOver({ active, over }) {
// In this specific use-case, the picked up item's `id` is always the same as the first `over` id.
// The first `onDragOver` event therefore doesn't need to be announced, because it is called
// immediately after the `onDragStart` announcement and is redundant.
if (isFirstAnnouncement.current === true) {
isFirstAnnouncement.current = false;
return;
}

if (over) {
return `Sortable item ${active.id} was moved into position ${getPosition(over.id)} of ${
items.length
}`;
}

return;
},
onDragEnd({ active, over }) {
if (over) {
return `Sortable item ${active.id} was dropped at position ${getPosition(over.id)} of ${
items.length
}`;
}

return;
},
onDragCancel({ active: { id } }) {
return `Sorting was cancelled. Sortable item ${id} was dropped and returned to position ${getPosition(
id,
)} of ${items.length}.`;
},
};

useEffect(() => {
if (!activeId) {
isFirstAnnouncement.current = true;
}
}, [activeId]);

return (
<DndContext
accessibility={{
announcements,
screenReaderInstructions,
}}
sensors={sensors}
collisionDetection={collisionDetection}
onDragStart={({ active }) => {
if (!active) {
return;
}

setActiveId(active.id);
}}
onDragEnd={({ over }) => {
setActiveId(null);

if (over) {
const overIndex = getIndex(over.id);
if (activeIndex !== overIndex) {
setItems((items) => reorderItems(items, activeIndex, overIndex));
}
}
}}
onDragCancel={() => setActiveId(null)}
measuring={measuring}
modifiers={modifiers}
>
<div>
<SortableContext items={items} strategy={strategy}>
<div>
{apps.map((value, index) => (
<>
<SortableAppItem {...value} />
</>
))}
</div>
</SortableContext>
</div>
</DndContext>
);
}

const SortableAppItem = ({
app: IApp,
disabled,
animateLayoutChanges,
getNewIndex,
handle,
id,
index,
onRemove,
style,
renderItem,
useDragOverlay,
wrapperStyle,
}) => {
/*const { transition, transform, attributes, setNodeRef, listeners } = useSortable({
id: app.id,
});*/

const {
active,
attributes,
isDragging,
isSorting,
listeners,
overIndex,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
} = useSortable({
id,
animateLayoutChanges,
disabled,
getNewIndex,
});

const style = {
transform: CSS.Transform.toString(transform),
transition,
};

return (
<div ref={setNodeRef} {...attributes} {...listeners} style={style}>
<AppIcon {...app} />
</div>
);
};
Loading

0 comments on commit 6a4185c

Please sign in to comment.