Skip to content

Commit a62a2ac

Browse files
committed
refactor: orderlist and picklist POC
1 parent 25ebf52 commit a62a2ac

25 files changed

Lines changed: 2285 additions & 8 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
'use client';
2+
import { useOrderList } from '@primereact/headless/orderlist2';
3+
import { Button } from '@primereact/ui/button';
4+
import { Listbox, type ListboxRootValueChangeEvent } from '@primereact/ui/listbox';
5+
import { cn } from '@primeuix/utils';
6+
import * as React from 'react';
7+
8+
interface Product {
9+
id: string;
10+
name: string;
11+
category: string;
12+
}
13+
14+
const initialProducts: Product[] = [
15+
{ id: '1', name: 'Bamboo Watch', category: 'Accessories' },
16+
{ id: '2', name: 'Black Watch', category: 'Accessories' },
17+
{ id: '3', name: 'Blue Band', category: 'Fitness' },
18+
{ id: '4', name: 'Blue T-Shirt', category: 'Clothing' },
19+
{ id: '5', name: 'Bracelet', category: 'Accessories' },
20+
{ id: '6', name: 'Brown Purse', category: 'Accessories' },
21+
{ id: '7', name: 'Chakra Bracelet', category: 'Accessories' },
22+
{ id: '8', name: 'Galaxy Earrings', category: 'Accessories' }
23+
];
24+
25+
const ITEM_HEIGHT = 37;
26+
27+
/**
28+
* The dragged item stays at its original slot (invisible placeholder).
29+
* Items between draggedIndex and dragOverIndex slide by ±ITEM_HEIGHT
30+
* to open a visible gap exactly at the drop target position.
31+
*/
32+
function getDragShift(index: number, draggedIndex: number | null, dragOverIndex: number | null): number {
33+
if (draggedIndex === null || dragOverIndex === null) return 0;
34+
if (draggedIndex === dragOverIndex) return 0;
35+
if (index === draggedIndex) return 0; // stays in place as invisible placeholder
36+
37+
if (draggedIndex < dragOverIndex) {
38+
if (index > draggedIndex && index <= dragOverIndex) return -ITEM_HEIGHT;
39+
} else {
40+
if (index >= dragOverIndex && index < draggedIndex) return ITEM_HEIGHT;
41+
}
42+
43+
return 0;
44+
}
45+
46+
export default function BasicDemo() {
47+
const [selectedItem, setSelectedItem] = React.useState<Product | null>(null);
48+
49+
const { items, dragState, methods, getListHandlers, getItemHandlers } = useOrderList({
50+
defaultItems: initialProducts,
51+
itemKey: (item) => item.id
52+
});
53+
54+
const selectedIndex = selectedItem ? items.findIndex((i) => i.id === selectedItem.id) : -1;
55+
const isAtTop = selectedIndex === 0;
56+
const isAtBottom = selectedIndex === items.length - 1;
57+
58+
return (
59+
<div className="flex flex-col gap-4 max-w-sm mx-auto">
60+
<div className="flex gap-2 flex-wrap">
61+
<Button
62+
severity="secondary"
63+
size="small"
64+
disabled={!selectedItem || isAtTop}
65+
onClick={() => selectedItem && methods.moveTop(selectedItem)}
66+
>
67+
Top
68+
</Button>
69+
<Button
70+
severity="secondary"
71+
size="small"
72+
disabled={!selectedItem || isAtTop}
73+
onClick={() => selectedItem && methods.moveUp(selectedItem)}
74+
>
75+
Up
76+
</Button>
77+
<Button
78+
severity="secondary"
79+
size="small"
80+
disabled={!selectedItem || isAtBottom}
81+
onClick={() => selectedItem && methods.moveDown(selectedItem)}
82+
>
83+
Down
84+
</Button>
85+
<Button
86+
severity="secondary"
87+
size="small"
88+
disabled={!selectedItem || isAtBottom}
89+
onClick={() => selectedItem && methods.moveBottom(selectedItem)}
90+
>
91+
Bottom
92+
</Button>
93+
</div>
94+
95+
<div className="border rounded-md overflow-hidden border-(--p-content-border-color)" {...getListHandlers()}>
96+
<Listbox.Root
97+
options={items}
98+
optionKey="id"
99+
value={selectedItem}
100+
onValueChange={(e: ListboxRootValueChangeEvent) => setSelectedItem(e.value as Product | null)}
101+
aria-label="Order list"
102+
>
103+
<Listbox.List>
104+
{items.map((item, index) => {
105+
const isDragging = dragState.draggedIndex === index;
106+
const shift = getDragShift(index, dragState.draggedIndex, dragState.dragOverIndex);
107+
108+
return (
109+
<Listbox.Option
110+
key={item.id}
111+
uKey={item.id}
112+
index={index}
113+
pt={{
114+
root: {
115+
...getItemHandlers(index),
116+
draggable: true,
117+
style: {
118+
cursor: isDragging ? 'grabbing' : 'grab',
119+
opacity: isDragging ? 0 : 1,
120+
transform: shift !== 0 ? `translateY(${shift}px)` : undefined,
121+
transition: isDragging ? undefined : 'transform 200ms ease-out, opacity 200ms ease-out',
122+
position: 'relative'
123+
}
124+
}
125+
}}
126+
>
127+
<span
128+
className={cn('text-xs tabular-nums w-5 shrink-0', isDragging ? 'opacity-50' : 'text-(--p-text-muted-color)')}
129+
>
130+
{String(index + 1).padStart(2, '0')}
131+
</span>
132+
<span className="flex-1">{item.name}</span>
133+
<span className="text-(--p-text-muted-color) text-xs">{item.category}</span>
134+
</Listbox.Option>
135+
);
136+
})}
137+
</Listbox.List>
138+
</Listbox.Root>
139+
</div>
140+
</div>
141+
);
142+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client';
2+
import { Search } from '@primeicons/react/search';
3+
import { useOrderList } from '@primereact/headless/orderlist2';
4+
import { Button } from '@primereact/ui/button';
5+
import { IconField } from '@primereact/ui/iconfield';
6+
import { InputText } from '@primereact/ui/inputtext';
7+
import { Listbox, type ListboxRootValueChangeEvent } from '@primereact/ui/listbox';
8+
import * as React from 'react';
9+
10+
interface Product {
11+
id: string;
12+
name: string;
13+
category: string;
14+
}
15+
16+
const initialProducts: Product[] = [
17+
{ id: '1', name: 'Bamboo Watch', category: 'Accessories' },
18+
{ id: '2', name: 'Black Watch', category: 'Accessories' },
19+
{ id: '3', name: 'Blue Band', category: 'Fitness' },
20+
{ id: '4', name: 'Blue T-Shirt', category: 'Clothing' },
21+
{ id: '5', name: 'Bracelet', category: 'Accessories' },
22+
{ id: '6', name: 'Brown Purse', category: 'Accessories' },
23+
{ id: '7', name: 'Chakra Bracelet', category: 'Accessories' },
24+
{ id: '8', name: 'Galaxy Earrings', category: 'Accessories' },
25+
{ id: '9', name: 'Game Controller', category: 'Electronics' },
26+
{ id: '10', name: 'Gold Phone Case', category: 'Accessories' },
27+
{ id: '11', name: 'Green Earbuds', category: 'Electronics' },
28+
{ id: '12', name: 'Green T-Shirt', category: 'Clothing' }
29+
];
30+
31+
const ITEM_HEIGHT = 37;
32+
33+
function getDragShift(index: number, draggedIndex: number | null, dragOverIndex: number | null): number {
34+
if (draggedIndex === null || dragOverIndex === null || index === draggedIndex) return 0;
35+
36+
if (draggedIndex < dragOverIndex) {
37+
if (index > draggedIndex && index <= dragOverIndex) return -ITEM_HEIGHT;
38+
} else {
39+
if (index >= dragOverIndex && index < draggedIndex) return ITEM_HEIGHT;
40+
}
41+
42+
return 0;
43+
}
44+
45+
export default function FilterDemo() {
46+
const [filter, setFilter] = React.useState('');
47+
const [selectedItem, setSelectedItem] = React.useState<Product | null>(null);
48+
49+
const { items, dragState, methods, getListHandlers, getItemHandlers } = useOrderList<Product>({
50+
defaultItems: initialProducts,
51+
itemKey: (item) => item.id
52+
});
53+
54+
const filteredItems = React.useMemo(() => items.filter((p) => p.name.toLowerCase().includes(filter.toLowerCase())), [items, filter]);
55+
56+
const isDragEnabled = !filter;
57+
58+
React.useEffect(() => {
59+
if (selectedItem && !filteredItems.some((i) => i.id === selectedItem.id)) {
60+
setSelectedItem(null);
61+
}
62+
}, [filteredItems, selectedItem]);
63+
64+
return (
65+
<div className="flex flex-col gap-4 max-w-sm mx-auto">
66+
<div className="flex gap-2 flex-wrap">
67+
<Button severity="secondary" size="small" disabled={!selectedItem} onClick={() => selectedItem && methods.moveTop(selectedItem)}>
68+
Top
69+
</Button>
70+
<Button severity="secondary" size="small" disabled={!selectedItem} onClick={() => selectedItem && methods.moveUp(selectedItem)}>
71+
Up
72+
</Button>
73+
<Button severity="secondary" size="small" disabled={!selectedItem} onClick={() => selectedItem && methods.moveDown(selectedItem)}>
74+
Down
75+
</Button>
76+
<Button severity="secondary" size="small" disabled={!selectedItem} onClick={() => selectedItem && methods.moveBottom(selectedItem)}>
77+
Bottom
78+
</Button>
79+
</div>
80+
81+
<div className="border rounded-md overflow-hidden border-(--p-content-border-color)" {...(isDragEnabled ? getListHandlers() : {})}>
82+
<div className="p-2 border-b border-(--p-content-border-color)">
83+
<IconField.Root>
84+
<InputText placeholder="Search..." value={filter} onChange={(e) => setFilter(e.target.value)} className="w-full" />
85+
<IconField.Inset>
86+
<Search />
87+
</IconField.Inset>
88+
</IconField.Root>
89+
</div>
90+
91+
<div className="flex items-center justify-between px-3 py-1.5 bg-(--p-surface-50) dark:bg-(--p-surface-800) border-b border-(--p-content-border-color)">
92+
<span className="text-xs text-(--p-text-muted-color)">
93+
{filteredItems.length}/{items.length} items
94+
</span>
95+
{filter && <span className="text-xs text-(--p-text-muted-color) italic">Move buttons work on full list position</span>}
96+
</div>
97+
98+
{filteredItems.length === 0 ? (
99+
<div className="flex items-center justify-center min-h-48 text-(--p-text-muted-color) text-sm select-none">No results found</div>
100+
) : (
101+
<Listbox.Root
102+
options={filteredItems}
103+
optionKey="id"
104+
value={selectedItem}
105+
onValueChange={(e: ListboxRootValueChangeEvent) => setSelectedItem(e.value as Product | null)}
106+
aria-label="Order list"
107+
>
108+
<Listbox.List style={{ maxHeight: '260px' }}>
109+
{filteredItems.map((item, filteredIndex) => {
110+
const fullIndex = items.indexOf(item);
111+
const isDragging = dragState.draggedIndex === fullIndex;
112+
const shift = isDragEnabled ? getDragShift(fullIndex, dragState.draggedIndex, dragState.dragOverIndex) : 0;
113+
114+
return (
115+
<Listbox.Option
116+
key={item.id}
117+
uKey={item.id}
118+
index={filteredIndex}
119+
pt={{
120+
root: {
121+
...(isDragEnabled ? getItemHandlers(fullIndex) : {}),
122+
style: {
123+
cursor: isDragEnabled ? 'grab' : 'default',
124+
opacity: isDragging ? 0.3 : 1,
125+
transform: shift !== 0 ? `translateY(${shift}px)` : undefined,
126+
transition: 'transform 180ms cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 150ms ease',
127+
position: 'relative'
128+
}
129+
}
130+
}}
131+
>
132+
<span className="text-xs tabular-nums w-5 shrink-0 text-(--p-text-muted-color)">
133+
{String(fullIndex + 1).padStart(2, '0')}
134+
</span>
135+
<span className="flex-1 text-sm">{item.name}</span>
136+
<span className="text-(--p-text-muted-color) text-xs">{item.category}</span>
137+
</Listbox.Option>
138+
);
139+
})}
140+
</Listbox.List>
141+
</Listbox.Root>
142+
)}
143+
</div>
144+
</div>
145+
);
146+
}

0 commit comments

Comments
 (0)