Skip to content

Commit 072ac1e

Browse files
committed
feat: add PickList and OrderList components
1 parent a62a2ac commit 072ac1e

38 files changed

Lines changed: 3970 additions & 648 deletions

apps/showcase/__store__/index.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,6 +1700,20 @@ export const Store: Record<string, Record<string, Record<string, { component: Re
17001700
'filePath': 'demo/styled/metergroup/vertical-demo.tsx',
17011701
},
17021702
},
1703+
'orderlist': {
1704+
'basic-demo': {
1705+
'component': React.lazy(() => import('demo/styled/orderlist/basic-demo')),
1706+
'filePath': 'demo/styled/orderlist/basic-demo.tsx',
1707+
},
1708+
'checkbox-demo': {
1709+
'component': React.lazy(() => import('demo/styled/orderlist/checkbox-demo')),
1710+
'filePath': 'demo/styled/orderlist/checkbox-demo.tsx',
1711+
},
1712+
'drag-drop-demo': {
1713+
'component': React.lazy(() => import('demo/styled/orderlist/drag-drop-demo')),
1714+
'filePath': 'demo/styled/orderlist/drag-drop-demo.tsx',
1715+
},
1716+
},
17031717
'orgchart': {
17041718
'basic-demo': {
17051719
'component': React.lazy(() => import('demo/styled/orgchart/basic-demo')),
@@ -1848,6 +1862,24 @@ export const Store: Record<string, Record<string, Record<string, { component: Re
18481862
'filePath': 'demo/styled/password/toggle-mask-demo.tsx',
18491863
},
18501864
},
1865+
'picklist': {
1866+
'basic-demo': {
1867+
'component': React.lazy(() => import('demo/styled/picklist/basic-demo')),
1868+
'filePath': 'demo/styled/picklist/basic-demo.tsx',
1869+
},
1870+
'checkbox-demo': {
1871+
'component': React.lazy(() => import('demo/styled/picklist/checkbox-demo')),
1872+
'filePath': 'demo/styled/picklist/checkbox-demo.tsx',
1873+
},
1874+
'custom-demo': {
1875+
'component': React.lazy(() => import('demo/styled/picklist/custom-demo')),
1876+
'filePath': 'demo/styled/picklist/custom-demo.tsx',
1877+
},
1878+
'drag-drop-demo': {
1879+
'component': React.lazy(() => import('demo/styled/picklist/drag-drop-demo')),
1880+
'filePath': 'demo/styled/picklist/drag-drop-demo.tsx',
1881+
},
1882+
},
18511883
'popover': {
18521884
'accessibility-demo': {
18531885
'component': React.lazy(() => import('demo/styled/popover/accessibility-demo')),

apps/showcase/app/(app)/playground/page.tsx

Lines changed: 405 additions & 0 deletions
Large diffs are not rendered by default.

apps/showcase/assets/menu/submenu/menu-styled.data.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export const styledMenu = [
156156
name: 'DataView',
157157
href: '/docs/styled/components/dataview'
158158
},
159+
{
160+
name: 'OrderList',
161+
href: '/docs/styled/components/orderlist'
162+
},
159163
{
160164
name: 'OrgChart',
161165
href: '/docs/styled/components/orgchart'
@@ -164,6 +168,10 @@ export const styledMenu = [
164168
name: 'Paginator',
165169
href: '/docs/styled/components/paginator'
166170
},
171+
{
172+
name: 'PickList',
173+
href: '/docs/styled/components/picklist'
174+
},
167175
{
168176
name: 'Timeline',
169177
href: '/docs/styled/components/timeline'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client';
2+
import { AngleDoubleDown } from '@primeicons/react/angle-double-down';
3+
import { AngleDoubleUp } from '@primeicons/react/angle-double-up';
4+
import { AngleDown } from '@primeicons/react/angle-down';
5+
import { AngleUp } from '@primeicons/react/angle-up';
6+
import { useOrderList } from '@primereact/headless/orderlist';
7+
import type { useOrderListReorderEvent } from '@primereact/types/shared/orderlist';
8+
import { Button } from '@primereact/ui/button';
9+
import { Listbox } from '@primereact/ui/listbox';
10+
import { useState } from 'react';
11+
12+
const cities = [
13+
{ name: 'New York', code: 'NY' },
14+
{ name: 'Rome', code: 'RM' },
15+
{ name: 'London', code: 'LDN' },
16+
{ name: 'Istanbul', code: 'IST' },
17+
{ name: 'Paris', code: 'PRS' }
18+
];
19+
20+
export default function BasicDemo() {
21+
const [items, setItems] = useState(cities);
22+
const [selection, setSelection] = useState<string[]>([]);
23+
24+
const orderList = useOrderList({
25+
value: items,
26+
selection,
27+
dataKey: 'code',
28+
optionValue: 'code',
29+
onReorder: (e: useOrderListReorderEvent) => setItems(e.value as typeof cities)
30+
});
31+
32+
return (
33+
<div className="flex justify-center">
34+
<div className="w-full md:w-56">
35+
<div className="flex gap-2">
36+
<div className="flex flex-col gap-1">
37+
<Button {...orderList.firstProps} severity="secondary" size="small" iconOnly aria-label="Move to first">
38+
<AngleDoubleUp />
39+
</Button>
40+
<Button {...orderList.prevProps} severity="secondary" size="small" iconOnly aria-label="Move up">
41+
<AngleUp />
42+
</Button>
43+
<Button {...orderList.nextProps} severity="secondary" size="small" iconOnly aria-label="Move down">
44+
<AngleDown />
45+
</Button>
46+
<Button {...orderList.lastProps} severity="secondary" size="small" iconOnly aria-label="Move to last">
47+
<AngleDoubleDown />
48+
</Button>
49+
</div>
50+
<Listbox.Root
51+
value={selection}
52+
onValueChange={(e) => setSelection((e.value as string[]) ?? [])}
53+
options={orderList.state.value as typeof cities}
54+
optionLabel="name"
55+
optionValue="code"
56+
multiple
57+
className="flex-1"
58+
>
59+
<Listbox.List />
60+
</Listbox.Root>
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use client';
2+
import { AngleDoubleDown } from '@primeicons/react/angle-double-down';
3+
import { AngleDoubleUp } from '@primeicons/react/angle-double-up';
4+
import { AngleDown } from '@primeicons/react/angle-down';
5+
import { AngleUp } from '@primeicons/react/angle-up';
6+
import { Check } from '@primeicons/react';
7+
import { useOrderList } from '@primereact/headless/orderlist';
8+
import type { useOrderListReorderEvent } from '@primereact/types/shared/orderlist';
9+
import { Button } from '@primereact/ui/button';
10+
import { Checkbox } from '@primereact/ui/checkbox';
11+
import { Listbox, type ListboxListInstance } from '@primereact/ui/listbox';
12+
import { cn } from '@primeuix/utils';
13+
import { useState, useRef, useEffect } from 'react';
14+
15+
const cities = [
16+
{ name: 'New York', code: 'NY' },
17+
{ name: 'Rome', code: 'RM' },
18+
{ name: 'London', code: 'LDN' },
19+
{ name: 'Istanbul', code: 'IST' },
20+
{ name: 'Paris', code: 'PRS' }
21+
];
22+
23+
const optionClassName = cn(
24+
'cursor-grab select-none transition-transform duration-200',
25+
'data-dragging:transition-none data-dragging:fixed data-dragging:z-50 data-dragging:cursor-grabbing data-dragging:shadow-xl data-dragging:pointer-events-none',
26+
'data-dragging:w-(--width) data-dragging:h-(--height) data-dragging:left-(--left) data-dragging:top-(--top)',
27+
'data-dragging:translate-x-(--dnd-x) data-dragging:translate-y-(--dnd-y)',
28+
'data-dropping:transition-[translate] data-dropping:duration-200 data-dropping:ease-out'
29+
);
30+
31+
export default function CheckboxDemo() {
32+
const [items, setItems] = useState(cities);
33+
const [selection, setSelection] = useState<string[]>([]);
34+
35+
const orderList = useOrderList({
36+
value: items,
37+
selection,
38+
dataKey: 'code',
39+
optionValue: 'code',
40+
draggable: true,
41+
onReorder: (e: useOrderListReorderEvent) => setItems(e.value as typeof cities)
42+
});
43+
44+
const wrapperRef = useRef<HTMLDivElement>(null);
45+
46+
useEffect(() => {
47+
const ul = wrapperRef.current?.querySelector('[role="listbox"]') as HTMLElement | null;
48+
49+
(orderList.listRef as React.RefObject<HTMLElement | null>).current = ul;
50+
});
51+
52+
return (
53+
<div className="flex justify-center">
54+
<div className="w-full md:w-56">
55+
<div className="flex gap-2">
56+
<div className="flex flex-col gap-1">
57+
<Button {...orderList.firstProps} severity="secondary" size="small" iconOnly aria-label="Move to first">
58+
<AngleDoubleUp />
59+
</Button>
60+
<Button {...orderList.prevProps} severity="secondary" size="small" iconOnly aria-label="Move up">
61+
<AngleUp />
62+
</Button>
63+
<Button {...orderList.nextProps} severity="secondary" size="small" iconOnly aria-label="Move down">
64+
<AngleDown />
65+
</Button>
66+
<Button {...orderList.lastProps} severity="secondary" size="small" iconOnly aria-label="Move to last">
67+
<AngleDoubleDown />
68+
</Button>
69+
</div>
70+
<div ref={wrapperRef} className="flex-1">
71+
<Listbox.Root
72+
value={selection}
73+
onValueChange={(e) => setSelection((e.value as string[]) ?? [])}
74+
options={orderList.state.value as typeof cities}
75+
optionLabel="name"
76+
optionValue="code"
77+
multiple
78+
>
79+
<Listbox.List style={{ touchAction: 'none' }}>
80+
{(instance: ListboxListInstance) => {
81+
const { listbox } = instance;
82+
83+
return orderList.state.value.map((item, index) => {
84+
const city = item as (typeof cities)[0];
85+
const selected = listbox?.isSelected(city);
86+
const { onPointerDown } = orderList.getOptionProps(item, index) as { onPointerDown?: React.PointerEventHandler<HTMLElement> };
87+
88+
return (
89+
<Listbox.Option key={city.code} uKey={city.code} index={index} onPointerDown={onPointerDown} className={cn(optionClassName, 'gap-2')}>
90+
<Checkbox.Root defaultChecked={!!selected} tabIndex={-1} readOnly>
91+
<Checkbox.Box>
92+
<Checkbox.Indicator match="checked">
93+
<Check />
94+
</Checkbox.Indicator>
95+
</Checkbox.Box>
96+
</Checkbox.Root>
97+
{city.name}
98+
</Listbox.Option>
99+
);
100+
});
101+
}}
102+
</Listbox.List>
103+
</Listbox.Root>
104+
</div>
105+
</div>
106+
</div>
107+
</div>
108+
);
109+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use client';
2+
import { AngleDoubleDown } from '@primeicons/react/angle-double-down';
3+
import { AngleDoubleUp } from '@primeicons/react/angle-double-up';
4+
import { AngleDown } from '@primeicons/react/angle-down';
5+
import { AngleUp } from '@primeicons/react/angle-up';
6+
import { useOrderList } from '@primereact/headless/orderlist';
7+
import type { useOrderListReorderEvent } from '@primereact/types/shared/orderlist';
8+
import { Button } from '@primereact/ui/button';
9+
import { Listbox } from '@primereact/ui/listbox';
10+
import { cn } from '@primeuix/utils';
11+
import { useState, useRef, useEffect } from 'react';
12+
13+
const cities = [
14+
{ name: 'New York', code: 'NY' },
15+
{ name: 'Rome', code: 'RM' },
16+
{ name: 'London', code: 'LDN' },
17+
{ name: 'Istanbul', code: 'IST' },
18+
{ name: 'Paris', code: 'PRS' }
19+
];
20+
21+
const optionClassName = cn(
22+
'cursor-grab select-none transition-transform duration-200',
23+
'data-dragging:transition-none data-dragging:fixed data-dragging:z-50 data-dragging:cursor-grabbing data-dragging:shadow-xl data-dragging:pointer-events-none',
24+
'data-dragging:w-(--width) data-dragging:h-(--height) data-dragging:left-(--left) data-dragging:top-(--top)',
25+
'data-dragging:translate-x-(--dnd-x) data-dragging:translate-y-(--dnd-y)',
26+
'data-dropping:transition-[translate] data-dropping:duration-200 data-dropping:ease-out'
27+
);
28+
29+
export default function DragDropDemo() {
30+
const [items, setItems] = useState(cities);
31+
const [selection, setSelection] = useState<string[]>([]);
32+
33+
const orderList = useOrderList({
34+
value: items,
35+
selection,
36+
dataKey: 'code',
37+
optionValue: 'code',
38+
draggable: true,
39+
onReorder: (e: useOrderListReorderEvent) => setItems(e.value as typeof cities)
40+
});
41+
42+
const wrapperRef = useRef<HTMLDivElement>(null);
43+
44+
useEffect(() => {
45+
const ul = wrapperRef.current?.querySelector('[role="listbox"]') as HTMLElement | null;
46+
47+
(orderList.listRef as React.RefObject<HTMLElement | null>).current = ul;
48+
});
49+
50+
return (
51+
<div className="flex justify-center">
52+
<div className="w-full md:w-56">
53+
<div className="flex gap-2">
54+
<div className="flex flex-col gap-1">
55+
<Button {...orderList.firstProps} severity="secondary" size="small" iconOnly aria-label="Move to first">
56+
<AngleDoubleUp />
57+
</Button>
58+
<Button {...orderList.prevProps} severity="secondary" size="small" iconOnly aria-label="Move up">
59+
<AngleUp />
60+
</Button>
61+
<Button {...orderList.nextProps} severity="secondary" size="small" iconOnly aria-label="Move down">
62+
<AngleDown />
63+
</Button>
64+
<Button {...orderList.lastProps} severity="secondary" size="small" iconOnly aria-label="Move to last">
65+
<AngleDoubleDown />
66+
</Button>
67+
</div>
68+
<div ref={wrapperRef} className="flex-1">
69+
<Listbox.Root
70+
value={selection}
71+
onValueChange={(e) => setSelection((e.value as string[]) ?? [])}
72+
options={orderList.state.value as typeof cities}
73+
optionLabel="name"
74+
optionValue="code"
75+
multiple
76+
>
77+
<Listbox.List style={{ touchAction: 'none' }}>
78+
{orderList.state.value.map((item, index) => {
79+
const city = item as (typeof cities)[0];
80+
const { onPointerDown } = orderList.getOptionProps(item, index) as { onPointerDown?: React.PointerEventHandler<HTMLElement> };
81+
82+
return (
83+
<Listbox.Option key={city.code} uKey={city.code} index={index} onPointerDown={onPointerDown} className={optionClassName}>
84+
{city.name}
85+
</Listbox.Option>
86+
);
87+
})}
88+
</Listbox.List>
89+
</Listbox.Root>
90+
</div>
91+
</div>
92+
</div>
93+
</div>
94+
);
95+
}

0 commit comments

Comments
 (0)