Skip to content

Commit 6a671ee

Browse files
authored
feat(color-picker): reorder theme colors via drag and drop (#10713)
- Add @dnd-kit sortable list on config screen - Use F36 DragHandle as activator; 8px activation to avoid input conflicts - Keyboard reordering via dnd-kit KeyboardSensor - Unique ids per swatch row for a11y Made-with: Cursor
1 parent 4236faa commit 6a671ee

File tree

4 files changed

+158
-14
lines changed

4 files changed

+158
-14
lines changed

apps/color-picker/package-lock.json

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/color-picker/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"@contentful/f36-components": "4.67.0",
99
"@contentful/f36-tokens": "4.0.5",
1010
"@contentful/react-apps-toolkit": "1.2.16",
11+
"@dnd-kit/core": "^6.1.0",
12+
"@dnd-kit/sortable": "^8.0.0",
1113
"@emotion/css": "^11.13.0",
1214
"@tsconfig/create-react-app": "1.0.3",
1315
"@vitejs/plugin-legacy": "^5.4.2",

apps/color-picker/src/components/SwatchEditor.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Flex, FormControl, IconButton, TextInput } from '@contentful/f36-components';
1+
import { useSortable } from '@dnd-kit/sortable';
2+
import { CSS } from '@dnd-kit/utilities';
3+
import { DragHandle, Flex, FormControl, IconButton, TextInput } from '@contentful/f36-components';
24
import { DeleteIcon } from '@contentful/f36-icons';
35
import tokens from '@contentful/f36-tokens';
46
import { css } from 'emotion';
@@ -20,21 +22,48 @@ interface SwatchEditorProps {
2022
}
2123

2224
export default function SwatchEditor({ swatch, onChange, onRemove }: SwatchEditorProps) {
25+
const {
26+
attributes,
27+
listeners,
28+
setNodeRef,
29+
setActivatorNodeRef,
30+
transform,
31+
transition,
32+
isDragging,
33+
} = useSortable({ id: swatch.id });
34+
35+
const rowStyle = {
36+
transform: CSS.Transform.toString(transform),
37+
transition,
38+
};
39+
2340
return (
24-
<div>
41+
<div ref={setNodeRef} style={rowStyle}>
2542
<FormControl marginBottom="spacingM">
2643
<Flex gap={tokens.spacingXs} alignItems="center">
27-
<FormControl.Label htmlFor="SwatchEditor" className={styles.displayNone}>
44+
<DragHandle
45+
as="button"
46+
type="button"
47+
ref={setActivatorNodeRef}
48+
{...listeners}
49+
{...attributes}
50+
label="Reorder color in list"
51+
variant="transparent"
52+
isActive={isDragging}
53+
/>
54+
<FormControl.Label
55+
htmlFor={`SwatchEditorColor-${swatch.id}`}
56+
className={styles.displayNone}>
2857
Color
2958
</FormControl.Label>
3059
<input
3160
value={swatch.value}
3261
onChange={(e) => onChange({ ...swatch, value: e.target.value })}
33-
id="SwatchEditorColor"
62+
id={`SwatchEditorColor-${swatch.id}`}
3463
type="color"
3564
/>
3665
<TextInput
37-
name="SwatchEditorName"
66+
name={`SwatchEditorName-${swatch.id}`}
3867
placeholder="Color name"
3968
size="small"
4069
value={swatch.name}

apps/color-picker/src/locations/ConfigScreen.tsx

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
import {
2+
DndContext,
3+
DragEndEvent,
4+
KeyboardSensor,
5+
PointerSensor,
6+
closestCenter,
7+
useSensor,
8+
useSensors,
9+
} from '@dnd-kit/core';
10+
import {
11+
arrayMove,
12+
SortableContext,
13+
sortableKeyboardCoordinates,
14+
verticalListSortingStrategy,
15+
} from '@dnd-kit/sortable';
116
import { ConfigAppSDK } from '@contentful/app-sdk';
217
import {
318
Button,
@@ -47,6 +62,39 @@ const ConfigScreen = () => {
4762
});
4863
const sdk = useSDK<ConfigAppSDK>();
4964

65+
const sensors = useSensors(
66+
useSensor(PointerSensor, {
67+
activationConstraint: { distance: 8 },
68+
}),
69+
useSensor(KeyboardSensor, {
70+
coordinateGetter: sortableKeyboardCoordinates,
71+
})
72+
);
73+
74+
const handleColorsDragEnd = useCallback((event: DragEndEvent) => {
75+
const { active, over } = event;
76+
if (!over || active.id === over.id) {
77+
return;
78+
}
79+
setParameters((prev) => {
80+
const colors = prev.themes[0].colors;
81+
const oldIndex = colors.findIndex((c) => c.id === active.id);
82+
const newIndex = colors.findIndex((c) => c.id === over.id);
83+
if (oldIndex === -1 || newIndex === -1) {
84+
return prev;
85+
}
86+
return {
87+
...prev,
88+
themes: [
89+
{
90+
...prev.themes[0],
91+
colors: arrayMove(colors, oldIndex, newIndex),
92+
},
93+
],
94+
};
95+
});
96+
}, []);
97+
5098
const addSwatch = () => {
5199
setParameters({
52100
...parameters,
@@ -146,17 +194,27 @@ const ConfigScreen = () => {
146194
<div>
147195
<Subheading marginBottom="spacingXs">Theme</Subheading>
148196
<Paragraph>
149-
Optionally, specify a set of predefined colors that editors can choose from.
197+
Optionally, specify a set of predefined colors that editors can choose from. Drag the
198+
handle beside each row to change the order shown in the entry editor.
150199
</Paragraph>
151200

152-
{parameters.themes[0].colors.map((swatch) => (
153-
<SwatchEditor
154-
key={swatch.id}
155-
swatch={swatch}
156-
onChange={updateSwatch}
157-
onRemove={removeSwatch}
158-
/>
159-
))}
201+
<DndContext
202+
sensors={sensors}
203+
collisionDetection={closestCenter}
204+
onDragEnd={handleColorsDragEnd}>
205+
<SortableContext
206+
items={parameters.themes[0].colors.map((c) => c.id)}
207+
strategy={verticalListSortingStrategy}>
208+
{parameters.themes[0].colors.map((swatch) => (
209+
<SwatchEditor
210+
key={swatch.id}
211+
swatch={swatch}
212+
onChange={updateSwatch}
213+
onRemove={removeSwatch}
214+
/>
215+
))}
216+
</SortableContext>
217+
</DndContext>
160218

161219
<Button size="small" startIcon={<PlusIcon />} onClick={addSwatch}>
162220
Add color

0 commit comments

Comments
 (0)