Skip to content

Commit b706438

Browse files
committed
docs(drag-to-reorder): add an example to validate horizontal dragging works
currently we don't have any components that can be used for testing this
1 parent 32affed commit b706438

File tree

3 files changed

+262
-0
lines changed

3 files changed

+262
-0
lines changed

src/components/drag-handle/drag-handle.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { OpenDirection } from '../menu/menu.types';
2323
* :::
2424
*
2525
* @exampleComponent limel-example-drag-handle-basic
26+
* @exampleComponent limel-example-drag-handle-horizontal
2627
*
2728
* @private
2829
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@use '../../../style/mixins.scss';
2+
3+
:host {
4+
display: block;
5+
}
6+
7+
div {
8+
max-width: 100%;
9+
display: grid;
10+
grid-template-columns: 1fr 1fr 1fr;
11+
gap: 1rem;
12+
}
13+
14+
limel-drag-handle {
15+
margin-left: auto;
16+
}
17+
18+
limel-card {
19+
@include mixins.is-draggable-item(
20+
$border-radius-while-being-dragged: 0.95rem
21+
);
22+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { Component, h, Host, State } from '@stencil/core';
2+
import Sortable, { SortableEvent } from 'sortablejs';
3+
4+
interface KittenCard {
5+
id: string;
6+
image: {
7+
src: string;
8+
alt: string;
9+
};
10+
subheading: string;
11+
}
12+
13+
const DRAG_HANDLE_SELECTOR = '[data-drag-handle]';
14+
const DRAGGABLE_ITEM_SELECTOR = 'limel-card.kitten-card';
15+
const DEFAULT_DRAGGING_CLASS = 'is-being-dragged';
16+
const DEFAULT_CONTAINER_CLASS = 'has-an-item-which-is-being-dragged';
17+
const DEFAULT_DROP_ELEVATION_CLASS = 'is-elevated';
18+
const DROP_ELEVATION_DURATION = 1000;
19+
/**
20+
* Horizontal drag handle
21+
* This example shows how to use the drag handle component
22+
* together with SortableJS to enable horizontal drag-and-drop ordering.
23+
*/
24+
@Component({
25+
shadow: true,
26+
tag: 'limel-example-drag-handle-horizontal',
27+
styleUrl: 'drag-handle-horizontal.scss',
28+
})
29+
export class DragHandleHorizontalExample {
30+
@State()
31+
private kittens: KittenCard[] = [
32+
{
33+
id: 'kitten-1',
34+
image: {
35+
src: 'https://images.unsplash.com/photo-1621238224334-20222c044b84?q=80&w=600&h=400&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
36+
alt: 'Photo by https://unsplash.com/@paracetamol',
37+
},
38+
subheading: "@paracetamol's kitten",
39+
},
40+
{
41+
id: 'kitten-2',
42+
image: {
43+
src: 'https://images.unsplash.com/photo-1558674378-e4334d4fecc2?q=80&w=600&h=400&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
44+
alt: 'Photo by https://unsplash.com/@bliss1002',
45+
},
46+
subheading: "@bliss1002's kitten",
47+
},
48+
{
49+
id: 'kitten-3',
50+
image: {
51+
src: 'https://images.unsplash.com/photo-1692962063951-0b5c81887f3d?q=80&w=600&h=400&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
52+
alt: 'Photo by https://unsplash.com/@kmly',
53+
},
54+
subheading: "@kmly's kitten",
55+
},
56+
];
57+
58+
private container?: HTMLDivElement;
59+
private sortable?: Sortable;
60+
private dropElevationTimeout?: ReturnType<typeof setTimeout>;
61+
private dropElevationTarget?: HTMLElement;
62+
63+
public render() {
64+
return (
65+
<Host>
66+
<h4>Drag & sort these kittens</h4>
67+
<p>More cute → Less cute</p>
68+
<div class="kitten-list" ref={this.setContainer}>
69+
{this.kittens.map((kitten) => (
70+
<limel-card
71+
class="kitten-card"
72+
key={kitten.id}
73+
clickable={true}
74+
image={kitten.image}
75+
subheading={kitten.subheading}
76+
onClick={this.handleClick}
77+
data-reorder-id={kitten.id}
78+
>
79+
<limel-drag-handle
80+
dragDirection="horizontal"
81+
slot="component"
82+
/>
83+
</limel-card>
84+
))}
85+
</div>
86+
</Host>
87+
);
88+
}
89+
90+
private readonly handleClick = (event: MouseEvent) => {
91+
console.log('Card clicked', event.currentTarget);
92+
};
93+
94+
private readonly setContainer = (element?: HTMLDivElement) => {
95+
if (this.container === element) {
96+
return;
97+
}
98+
99+
this.teardownSortable();
100+
this.container = element ?? undefined;
101+
this.setupSortable();
102+
};
103+
104+
private setupSortable() {
105+
if (!this.container) {
106+
return;
107+
}
108+
109+
if (this.sortable) {
110+
this.sortable.option('handle', DRAG_HANDLE_SELECTOR);
111+
this.sortable.option('draggable', DRAGGABLE_ITEM_SELECTOR);
112+
this.sortable.option('disabled', false);
113+
return;
114+
}
115+
116+
this.sortable = Sortable.create(this.container, {
117+
animation: 150,
118+
direction: 'horizontal',
119+
handle: DRAG_HANDLE_SELECTOR,
120+
draggable: DRAGGABLE_ITEM_SELECTOR,
121+
chosenClass: DEFAULT_DRAGGING_CLASS,
122+
onStart: this.handleSortStart,
123+
onEnd: this.handleSortEnd,
124+
});
125+
}
126+
127+
private teardownSortable() {
128+
if (this.sortable) {
129+
this.sortable.destroy();
130+
this.sortable = undefined;
131+
}
132+
133+
this.clearDropElevationTimer();
134+
135+
if (this.dropElevationTarget) {
136+
this.dropElevationTarget.classList.remove(
137+
DEFAULT_DROP_ELEVATION_CLASS
138+
);
139+
this.dropElevationTarget = undefined;
140+
}
141+
142+
if (this.container) {
143+
this.container.classList.remove(DEFAULT_CONTAINER_CLASS);
144+
}
145+
}
146+
147+
private readonly handleSortStart = (event: SortableEvent) => {
148+
if (this.container) {
149+
this.container.classList.add(DEFAULT_CONTAINER_CLASS);
150+
}
151+
152+
if (event.item instanceof HTMLElement) {
153+
this.applyDropElevation(event.item);
154+
}
155+
};
156+
157+
private readonly handleSortEnd = (event: SortableEvent) => {
158+
if (this.container) {
159+
this.container.classList.remove(DEFAULT_CONTAINER_CLASS);
160+
}
161+
162+
const { oldIndex, newIndex } = event;
163+
164+
if (event.item instanceof HTMLElement) {
165+
this.dropElevationTarget = event.item;
166+
}
167+
168+
if (
169+
typeof oldIndex !== 'number' ||
170+
typeof newIndex !== 'number' ||
171+
oldIndex === newIndex
172+
) {
173+
this.scheduleDropElevationRemoval();
174+
return;
175+
}
176+
177+
this.kittens = this.moveKitten(this.kittens, oldIndex, newIndex);
178+
this.scheduleDropElevationRemoval();
179+
};
180+
181+
private applyDropElevation(item: HTMLElement) {
182+
if (this.dropElevationTarget && this.dropElevationTarget !== item) {
183+
this.dropElevationTarget.classList.remove(
184+
DEFAULT_DROP_ELEVATION_CLASS
185+
);
186+
}
187+
188+
this.clearDropElevationTimer();
189+
item.classList.add(DEFAULT_DROP_ELEVATION_CLASS);
190+
this.dropElevationTarget = item;
191+
}
192+
193+
private scheduleDropElevationRemoval() {
194+
if (!this.dropElevationTarget) {
195+
return;
196+
}
197+
198+
const target = this.dropElevationTarget;
199+
this.clearDropElevationTimer();
200+
this.dropElevationTimeout = globalThis.setTimeout(() => {
201+
target.classList.remove(DEFAULT_DROP_ELEVATION_CLASS);
202+
if (this.dropElevationTarget === target) {
203+
this.dropElevationTarget = undefined;
204+
}
205+
this.dropElevationTimeout = undefined;
206+
}, DROP_ELEVATION_DURATION);
207+
}
208+
209+
private clearDropElevationTimer() {
210+
if (this.dropElevationTimeout !== undefined) {
211+
clearTimeout(this.dropElevationTimeout);
212+
this.dropElevationTimeout = undefined;
213+
}
214+
}
215+
216+
private moveKitten(
217+
list: KittenCard[],
218+
fromIndex: number,
219+
toIndex: number
220+
): KittenCard[] {
221+
if (fromIndex === toIndex) {
222+
return list;
223+
}
224+
225+
const updated = [...list];
226+
const [moved] = updated.splice(fromIndex, 1);
227+
228+
if (!moved) {
229+
return list;
230+
}
231+
232+
updated.splice(toIndex, 0, moved);
233+
return updated;
234+
}
235+
236+
public disconnectedCallback() {
237+
this.teardownSortable();
238+
}
239+
}

0 commit comments

Comments
 (0)