Skip to content

Commit

Permalink
draggable
Browse files Browse the repository at this point in the history
  • Loading branch information
glweems committed Nov 28, 2020
1 parent 328b77f commit 5398cdf
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 193 deletions.
Empty file removed Grid/GridAreas.tsx
Empty file.
169 changes: 92 additions & 77 deletions Grid/GridControlProperties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,95 +3,110 @@ import theme, { colors } from '@lib/theme';
import { gridUnits } from '@lib/utils';
import { GrabberIcon, XIcon } from '@primer/octicons-react';
import { useShiftKeyPressed } from '@ui/useShftKeyPressed';
import React, { CSSProperties, FC, memo, useCallback } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { Entry } from 'css-grid-template-parser';
import React, { CSSProperties, memo, useCallback } from 'react';
import { SortableElement, SortableHandle } from 'react-sortable-hoc';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { GridControlObjKey } from './GridControlId';
import { gridControlState, selectedControlState } from './gridState';
type Handler = (
e: React.ChangeEvent<HTMLInputElement & HTMLSelectElement>
) => void;

const GridControlProperties: FC<{
type GridControlPropertiesProps = Entry & {
id: string | `${GridControlObjKey}.${number}`;
}> = ({ id }) => {
const { canDelete, ...control } = useRecoilValue(gridControlState(id));
const [selectedIds, setSelectedIds] = useRecoilState(selectedControlState);
const setControl = useSetRecoilState(gridControlState(id));
const shiftKeyPressed = useShiftKeyPressed();
const handleChange: Handler = useCallback(
(e) => {
setControl((prev) => ({
...prev,
[e.currentTarget.name]: e.currentTarget.value,
}));
},
[setControl]
);
const isSelected = selectedIds.includes(id);
const style: CSSProperties = isSelected
? id.split('.').includes('rows')
? {
background: colors.yellow[4],
boxShadow: `0 2px 0 0 ${colors.yellow[8]}`,
}
: {
background: colors.blue[4],
boxShadow: `0 2px 0 0 ${colors.blue[8]}`,
}
: { background: 'transparent' };
canDelete: boolean;
};

const onEnter = useCallback(() => {
setSelectedIds((ids) => {
if (isSelected) return ids;
if (shiftKeyPressed) return [...ids, id];
return [id];
});
}, [id, isSelected, setSelectedIds, shiftKeyPressed]);
const GridControlProperties = SortableElement(
({ id, amount, unit, canDelete }: GridControlPropertiesProps) => {
const [selectedIds, setSelectedIds] = useRecoilState(selectedControlState);
const setControl = useSetRecoilState(gridControlState(id));
const shiftKeyPressed = useShiftKeyPressed();
const handleChange: Handler = useCallback(
(e) => {
setControl((prev) => ({
...prev,
[e.currentTarget.name]: e.currentTarget.value,
}));
},
[setControl]
);
const isSelected = selectedIds.includes(id);
const style: CSSProperties = isSelected
? id.split('.').includes('rows')
? {
background: colors.yellow[4],
boxShadow: `0 2px 0 0 ${colors.yellow[8]}`,
}
: {
background: colors.blue[4],
boxShadow: `0 2px 0 0 ${colors.blue[8]}`,
}
: { background: 'transparent' };

const onLeave = useCallback(() => {
if (!shiftKeyPressed) setSelectedIds([]);
}, [setSelectedIds, shiftKeyPressed]);
const onEnter = useCallback(() => {
setSelectedIds((ids) => {
if (isSelected) return ids;
if (shiftKeyPressed) return [...ids, id];
return [id];
});
}, [id, isSelected, setSelectedIds, shiftKeyPressed]);

return (
<div
style={{ ...gridPropertiesStyles, ...style }}
css={`
&:focus,
:focus-within {
outline: 4px dashed ${colors.focus};
outline-offset: 4px;
}
`}
onPointerEnter={onEnter}
onPointerLeave={onLeave}
>
<div>
<GrabberIcon />
</div>
const onLeave = useCallback(() => {
if (!shiftKeyPressed) setSelectedIds([]);
}, [setSelectedIds, shiftKeyPressed]);

<input
autoComplete="off"
name="amount"
type="number"
value={control.amount}
onChange={handleChange}
/>
<Select
name="unit"
value={control.unit}
onChange={handleChange}
options={gridUnits}
/>
<button
className="close-btn"
disabled={canDelete}
onMouseDown={() => setControl(null)}
return (
<div
style={{ ...gridPropertiesStyles, ...style }}
css={`
&:focus,
:focus-within {
outline: 4px dashed ${colors.focus};
outline-offset: 4px;
}
`}
onPointerEnter={onEnter}
onPointerLeave={onLeave}
>
<XIcon size={28} />
</button>
</div>
);
};
<DragHandle disabled={canDelete} />
<input
autoComplete="off"
name="amount"
type="number"
value={amount}
onChange={handleChange}
/>
<Select
name="unit"
value={unit}
onChange={handleChange}
options={gridUnits}
/>
<button
className="close-btn"
disabled={canDelete}
onMouseDown={() => setControl(null)}
>
<XIcon size={28} />
</button>
</div>
);
}
);

const DragHandle = SortableHandle(({ disabled }) => (
<div
style={{
cursor: disabled ? 'null' : 'grab',
justifySelf: 'stretch',
color: disabled ? colors.baseGlare : colors.white,
}}
>
<GrabberIcon />
</div>
));

export const gridPropertiesStyles: CSSProperties = {
display: 'grid',
Expand Down
86 changes: 64 additions & 22 deletions Grid/GridControls.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import theme from '@lib/theme';
import { PlusIcon } from '@primer/octicons-react';
import { AnimatePresence, motion } from 'framer-motion';
import arrayMove from 'array-move';
import { Entry } from 'css-grid-template-parser';
import { motion } from 'framer-motion';
import { capitalize } from 'lodash';
import React, { FC, memo } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import styled from 'styled-components';
import React, { FC, memo, useState } from 'react';
import {
SortableContainer,
SortableContainerProps,
SortEndHandler,
SortStartHandler,
} from 'react-sortable-hoc';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { GridControlObjKey } from './GridControlId';
import GridControlProperties from './GridControlProperties';
import { gridControlsState } from './gridState';
import { gridControlsState, gridState } from './gridState';

type GridControlsProps = {
id: GridControlObjKey;
Expand All @@ -17,8 +24,22 @@ const GridControls: FC<GridControlsProps> = ({ id }) => {
const controls = useRecoilValue(gridControlsState(id));
const setControls = useSetRecoilState(gridControlsState(id));
const handleAdd = () => setControls(controls);
const [grid, setGrid] = useRecoilState(gridState);
const [activeDrag, setActiveDragIndex] = useState<number>();
const onSortStart: SortStartHandler = ({ index }) => {
console.log('activeDrag: ', activeDrag);
setActiveDragIndex(index);
};
const onSortEnd: SortEndHandler = ({ oldIndex, newIndex }) => {
console.log('activeDrag: ', activeDrag);
setActiveDragIndex(undefined);
setGrid((prev) => ({
...prev,
[id]: arrayMove(prev[id], oldIndex, newIndex),
}));
};
return (
<GridControlStyles>
<fieldset>
<legend>
<span>Grid Template {capitalize(id)}</span>
<button
Expand All @@ -29,24 +50,45 @@ const GridControls: FC<GridControlsProps> = ({ id }) => {
<PlusIcon /> {id}
</button>
</legend>
<AnimatePresence>
{controls?.map((_control, index) => (
<motion.div
key={`${id}.${index}`}
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<GridControlProperties
id={`${id as GridControlObjKey}.${index as number}`}
/>
</motion.div>
))}
</AnimatePresence>
</GridControlStyles>
<SortableList
id={id}
axis="y"
lockAxis="y"
hideSortableGhost={true}
useDragHandle={true}
items={grid?.[id]}
updateBeforeSortStart={onSortStart}
onSortEnd={onSortEnd}
activeDrag={activeDrag}
disabled={grid?.[id].length === 1}
/>
</fieldset>
);
};

const GridControlStyles = styled.fieldset``;
type SortableListProps = {
items: Entry[];
id: GridControlObjKey;
disabled: boolean;
activeDrag: number;
} & SortableContainerProps;

const SortableList = SortableContainer(
({ id, items, disabled }: SortableListProps) => (
<motion.ul>
{items?.map((value, index) => (
<GridControlProperties
disabled={disabled}
canDelete={disabled}
id={`${id}.${index}`}
key={`item-${id}-${index}`}
index={index}
{...value}
/>
))}
</motion.ul>
)
);

GridControls.displayName = 'GridControls';
export default memo(GridControls);
1 change: 0 additions & 1 deletion Grid/GridGapControls.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Select from '@components/Select';
import theme from '@lib/theme';
import { gridGapUnits } from '@lib/utils';
import { GrabberIcon } from '@primer/octicons-react';
import { Entry, GridState } from 'css-grid-template-parser';
Expand Down
6 changes: 3 additions & 3 deletions Grid/gridState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export const gridControlState = selectorFamily<GridControlState, string>({
key: 'gridControlState',
get: (id) => ({ get }) => {
const [key, index] = id.split('.');
const stack = get(gridState)[key];
const control = stack[index];
return { ...control, canDelete: stack.length <= 1 };
const stack = get(gridState)?.[key];
const control = stack?.[index];
return control;
},
set: (id) => ({ set }, newValue) => {
const [key, index] = id.split('.');
Expand Down
68 changes: 0 additions & 68 deletions components/DraggableListItem.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions components/SortableItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import { SortableElement } from 'react-sortable-hoc';

const SortableItem = SortableElement(({ value }) => (
<li tabIndex={0}>{JSON.stringify(value)}</li>
));

export default SortableItem;
Loading

0 comments on commit 5398cdf

Please sign in to comment.