Skip to content

Commit

Permalink
Merge pull request #46 from glweems/draggable
Browse files Browse the repository at this point in the history
Draggable
  • Loading branch information
glweems committed Nov 28, 2020
2 parents 38f92f9 + 5398cdf commit 2e8db4f
Show file tree
Hide file tree
Showing 24 changed files with 542 additions and 311 deletions.
180 changes: 99 additions & 81 deletions Grid/GridControlProperties.tsx
Original file line number Diff line number Diff line change
@@ -1,102 +1,120 @@
import Select from '@components/Select';
import { colors } from '@lib/theme';
import theme, { colors } from '@lib/theme';
import { gridUnits } from '@lib/utils';
import { Grid } from '@primer/components';
import { GrabberIcon, XIcon } from '@primer/octicons-react';
import { useShiftKeyPressed } from '@ui/useShftKeyPressed';
import React, { 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 = 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) => {
// Do nothing if the element is already selected
if (isSelected) return ids;
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' };

// Add this element to the selection if shift is pressed
if (shiftKeyPressed) return [...ids, id];
const onEnter = useCallback(() => {
setSelectedIds((ids) => {
if (isSelected) return ids;
if (shiftKeyPressed) return [...ids, id];
return [id];
});
}, [id, isSelected, setSelectedIds, shiftKeyPressed]);

// Otherwise, make this one the only selected element
return [id];
});
}, [id, isSelected, setSelectedIds, shiftKeyPressed]);
const onLeave = useCallback(() => {
if (!shiftKeyPressed) setSelectedIds([]);
}, [setSelectedIds, shiftKeyPressed]);
return (
<Grid
gridTemplateColumns="auto repeat(3, 1fr)"
padding={2}
style={style}
justifyContent="start"
onPointerEnter={onEnter}
onPointerLeave={onLeave}
gridGap="0 0.5rem"
>
<div>
<GrabberIcon />
</div>
const onLeave = useCallback(() => {
if (!shiftKeyPressed) setSelectedIds([]);
}, [setSelectedIds, shiftKeyPressed]);

<input
className="btn"
autoComplete="off"
name="amount"
type="number"
value={control.amount}
onChange={handleChange}
return (
<div
style={{ ...gridPropertiesStyles, ...style }}
css={`
width: 100px;
&:focus,
:focus-within {
outline: 4px dashed ${colors.focus};
outline-offset: 4px;
}
`}
/>
<Select
className="btn"
name="unit"
value={control.unit}
onChange={handleChange}
options={gridUnits}
/>
<button
className="btn"
disabled={canDelete}
onMouseDown={() => setControl(null)}
onPointerEnter={onEnter}
onPointerLeave={onLeave}
>
<XIcon />
</button>
</Grid>
);
<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',
gridTemplateColumns: 'auto repeat(2, 1fr) auto',
padding: theme.space[2],
alignItems: 'center',
justifyContent: 'start',
columnGap: theme.space[2],
};

export default memo(GridControlProperties);
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);
34 changes: 15 additions & 19 deletions Grid/GridGapControls.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Select from '@components/Select';
import { gridGapUnits } from '@lib/utils';
import { Grid } from '@primer/components';
import { GrabberIcon } from '@primer/octicons-react';
import { Entry, GridState } from 'css-grid-template-parser';
import React, { FC, memo } from 'react';
import { selectorFamily, useRecoilValue, useSetRecoilState } from 'recoil';
import { gridPropertiesStyles } from './GridControlProperties';
import { gridState } from './gridState';

const gridGapState = selectorFamily<Entry, keyof GridState['gap']>({
Expand Down Expand Up @@ -43,15 +43,14 @@ const GridGapControl: FC<{ id: keyof GridState['gap'] }> = ({ id }) => {
const gridGap = useRecoilValue(gridGapState(id));
const setGridGap = useSetRecoilState(gridGapState(id));
return (
<Grid
gridTemplateColumns="auto auto auto"
padding={2}
justifyContent="start"
alignItems="start"
<div
style={{
...gridPropertiesStyles,
gridTemplateColumns: 'auto repeat(2,1fr) 3ch',
}}
// style={style}
// onPointerEnter={onEnter}
// onPointerLeave={onLeave}
gridGap="0 0.5rem"
>
<div>
<GrabberIcon />
Expand All @@ -63,27 +62,24 @@ const GridGapControl: FC<{ id: keyof GridState['gap'] }> = ({ id }) => {
autoComplete="off"
value={gridGap?.amount}
onChange={(event) => {
if (event.target === event.currentTarget)
return setGridGap({
...gridGap,
[event.currentTarget.name]: event.currentTarget.value,
});
return setGridGap({
...gridGap,
[event.currentTarget.name]: event.currentTarget.value,
});
}}
/>
<Select
name="unit"
className="btn"
value={gridGap?.unit}
options={gridGapUnits}
onChange={(event) => {
if (event.target === event.currentTarget)
setGridGap({
...gridGap,
[event.currentTarget.name]: event.currentTarget.value,
});
setGridGap({
...gridGap,
[event.currentTarget.name]: event.currentTarget.value,
});
}}
/>
</Grid>
</div>
);
};

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
Loading

1 comment on commit 2e8db4f

@vercel
Copy link

@vercel vercel bot commented on 2e8db4f Nov 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.