Skip to content

Commit 1bb16f8

Browse files
committed
refactor: move all props to headless and update some components
1 parent 91cb8e0 commit 1bb16f8

880 files changed

Lines changed: 7291 additions & 2884 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/showcase/__store__/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2025,6 +2025,10 @@ export const Store: Record<string, Record<string, Record<string, { component: Re
20252025
'component': React.lazy(() => import('demo/styled/select/focus-behavior-demo')),
20262026
'filePath': 'demo/styled/select/focus-behavior-demo.tsx',
20272027
},
2028+
'group-checkbox-filter-demo': {
2029+
'component': React.lazy(() => import('demo/styled/select/group-checkbox-filter-demo')),
2030+
'filePath': 'demo/styled/select/group-checkbox-filter-demo.tsx',
2031+
},
20282032
'group-demo': {
20292033
'component': React.lazy(() => import('demo/styled/select/group-demo')),
20302034
'filePath': 'demo/styled/select/group-demo.tsx',
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use client';
2+
import { Check } from '@primeicons/react/check';
3+
import { Search } from '@primeicons/react/search';
4+
import { Tag as TagIcon } from '@primeicons/react/tag';
5+
import type { ListboxListInstance } from '@primereact/types/shared/listbox';
6+
import type { SelectValueChangeEvent } from '@primereact/types/shared/select';
7+
import { Button } from '@primereact/ui/button';
8+
import { Checkbox } from '@primereact/ui/checkbox';
9+
import { Chip } from '@primereact/ui/chip';
10+
import { IconField } from '@primereact/ui/iconfield';
11+
import { InputText } from '@primereact/ui/inputtext';
12+
import { Select } from '@primereact/ui/select';
13+
import * as React from 'react';
14+
15+
const labelGroups = [
16+
{
17+
label: 'Critical',
18+
code: 'critical',
19+
items: [
20+
{ label: 'Security', value: 'security' },
21+
{
22+
label: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
23+
value: 'lorem'
24+
}
25+
]
26+
},
27+
{
28+
label: 'Optional',
29+
code: 'optional',
30+
items: [
31+
{ label: 'Feature', value: 'feature' },
32+
{ label: 'Other', value: 'other' }
33+
]
34+
}
35+
];
36+
37+
const allItems = labelGroups.flatMap((g) => g.items);
38+
39+
export default function GroupCheckboxFilterDemo() {
40+
const [selected, setSelected] = React.useState<string[]>(['security']);
41+
const [filterValue, setFilterValue] = React.useState<string>('');
42+
43+
const filteredGroups = React.useMemo(() => {
44+
if (!filterValue) return labelGroups;
45+
46+
return labelGroups
47+
.map((group) => ({
48+
...group,
49+
items: group.items.filter((item) => item.label.toLowerCase().includes(filterValue.toLowerCase()))
50+
}))
51+
.filter((group) => group.items.length > 0);
52+
}, [filterValue]);
53+
54+
const handleSelectAll = () => {
55+
const visibleValues = filteredGroups.flatMap((g) => g.items.map((i) => i.value));
56+
setSelected([...new Set([...selected, ...visibleValues])]);
57+
};
58+
59+
const handleClear = (e: React.MouseEvent) => {
60+
e.stopPropagation();
61+
setSelected([]);
62+
};
63+
64+
const getTriggerLabel = () => {
65+
if (selected.length === 0) return null;
66+
const first = allItems.find((i) => i.value === selected[0])?.label ?? selected[0];
67+
return selected.length > 1 ? `${first} +${selected.length - 1}` : first;
68+
};
69+
70+
return (
71+
<div className="flex justify-center">
72+
<Select.Root
73+
value={selected}
74+
onValueChange={(e: SelectValueChangeEvent) => setSelected(e.value as string[])}
75+
options={filteredGroups}
76+
optionLabel="label"
77+
optionValue="value"
78+
optionKey="value"
79+
optionGroupLabel="label"
80+
optionGroupChildren="items"
81+
multiple
82+
className="w-full md:w-64 border-0 bg-transparent shadow-none"
83+
>
84+
<Select.Trigger>
85+
<Select.Value placeholder="Select labels" className="p-0">
86+
{getTriggerLabel() && (
87+
<Chip.Root className="bg-blue-50 dark:bg-blue-950 **:text-blue-700 dark:**:text-blue-300">
88+
<Chip.Start>
89+
<TagIcon />
90+
</Chip.Start>
91+
<Chip.Label>{getTriggerLabel()}</Chip.Label>
92+
</Chip.Root>
93+
)}
94+
</Select.Value>
95+
</Select.Trigger>
96+
97+
<Select.Portal>
98+
<Select.Positioner>
99+
<Select.Popup>
100+
<Select.Header className="flex flex-col gap-2 p-2 border-b border-surface">
101+
<IconField.Root>
102+
<IconField.Inset>
103+
<Search />
104+
</IconField.Inset>
105+
<Select.Filter
106+
as={InputText}
107+
value={filterValue}
108+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterValue(e.target.value)}
109+
placeholder="Search..."
110+
/>
111+
</IconField.Root>
112+
<div className="flex items-center gap-1 px-1">
113+
<Button variant="text" size="small" onClick={handleSelectAll}>
114+
Select All
115+
</Button>
116+
<Button variant="text" size="small" severity="secondary" onClick={handleClear}>
117+
Clear
118+
</Button>
119+
</div>
120+
</Select.Header>
121+
122+
<Select.List style={{ maxHeight: '18rem' }}>
123+
{(instance: ListboxListInstance) => {
124+
const { listbox, options } = instance;
125+
126+
return (options as unknown[])?.map((option: unknown, index: number) => {
127+
if (listbox?.isOptionGroup(option)) {
128+
const group = (option as Record<string, unknown>).optionGroup as Record<string, string>;
129+
130+
return (
131+
<Select.Option
132+
key={index}
133+
index={index}
134+
group
135+
className="text-xs font-semibold text-surface-500 uppercase tracking-wide"
136+
>
137+
{group?.label}
138+
</Select.Option>
139+
);
140+
}
141+
142+
const isSelected = listbox?.isSelected(option) ?? false;
143+
144+
return (
145+
<Select.Option key={index} index={index} className="gap-2">
146+
<Checkbox.Root defaultChecked={isSelected} tabIndex={-1} readOnly>
147+
<Checkbox.Box>
148+
<Checkbox.Indicator match="checked">
149+
<Check />
150+
</Checkbox.Indicator>
151+
</Checkbox.Box>
152+
</Checkbox.Root>
153+
<span className="flex-1 truncate">{listbox?.getOptionLabel(option)}</span>
154+
</Select.Option>
155+
);
156+
});
157+
}}
158+
</Select.List>
159+
160+
<Select.Empty className="text-sm text-center py-4 text-surface-400">No labels found</Select.Empty>
161+
</Select.Popup>
162+
</Select.Positioner>
163+
</Select.Portal>
164+
</Select.Root>
165+
</div>
166+
);
167+
}

apps/showcase/docs/styled/components/autocomplete/features.mdx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,6 @@ Use `AutoComplete.Clear` inside the root to display a clear button that resets t
8181

8282
<DocDemoViewer name="autocomplete:clear-icon-demo" />
8383

84-
### Group
85-
86-
Options can be grouped using the `optionGroupLabel` and `optionGroupChildren` properties.
87-
88-
<DocDemoViewer name="autocomplete:group-demo" />
89-
90-
### Custom Group
91-
92-
Customize group headers with custom rendering by accessing the list instance inside `AutoComplete.List`.
93-
94-
<DocDemoViewer name="autocomplete:custom-group-demo" />
95-
9684
### Custom Option
9785

9886
Customize option content using a render function inside `AutoComplete.Option` that receives the option instance including the `selected` state.
@@ -105,6 +93,20 @@ Use the `forceSelection` property to validate manual input against the suggestio
10593

10694
<DocDemoViewer name="autocomplete:forceselection-demo" />
10795

96+
### Group
97+
98+
#### Simple
99+
100+
Options can be grouped using the `optionGroupLabel` and `optionGroupChildren` properties.
101+
102+
<DocDemoViewer name="autocomplete:group-demo" />
103+
104+
#### Custom
105+
106+
Customize group headers with custom rendering by accessing the list instance inside `AutoComplete.List`.
107+
108+
<DocDemoViewer name="autocomplete:custom-group-demo" />
109+
108110
### Arrow
109111

110112
Use `AutoComplete.Arrow` inside the popup to display an arrow pointing to the trigger element. Set `sideOffset` on `AutoComplete.Positioner` for spacing.
@@ -157,9 +159,7 @@ import { VisuallyHidden } from '@primereact/ui/visuallyhidden';
157159
<AutoComplete.Root>
158160
{/* ... */}
159161
<VisuallyHidden role="status" aria-live="polite" aria-atomic>
160-
{items.length > 0
161-
? `${items.length} suggestions available.`
162-
: 'No suggestions available.'}
162+
{items.length > 0 ? `${items.length} suggestions available.` : 'No suggestions available.'}
163163
</VisuallyHidden>
164164
</AutoComplete.Root>
165165
```

apps/showcase/docs/styled/components/select/features.mdx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,31 @@ Add a search field inside the popup using `Select.Header` with `Select.Filter` t
101101

102102
<DocDemoViewer name="select:filter-demo" />
103103

104+
### Custom Option
105+
106+
Customize option content using a render function inside `Select.Option` that receives the option instance including the `selected` state.
107+
108+
<DocDemoViewer name="select:option-demo" />
109+
104110
### Group
105111

112+
#### Simple
113+
106114
Options can be grouped using the `optionGroupLabel` and `optionGroupChildren` properties.
107115

108116
<DocDemoViewer name="select:group-demo" />
109117

110-
### Custom Group
118+
#### Custom
111119

112120
Customize group headers with custom rendering by accessing the list instance inside `Select.List`.
113121

114122
<DocDemoViewer name="select:custom-group-demo" />
115123

116-
### Custom Option
124+
#### Checkbox and Filter
117125

118-
Customize option content using a render function inside `Select.Option` that receives the option instance including the `selected` state.
126+
Combine grouped options, checkbox selection, and a filter to create a rich multi-select experience with a compact trigger that displays the selection count.
119127

120-
<DocDemoViewer name="select:option-demo" />
128+
<DocDemoViewer name="select:group-checkbox-filter-demo" />
121129

122130
### Arrow
123131

apps/showcase/registry.json

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

packages/@primereact/headless/src/autocomplete/useAutoComplete.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export const useAutoComplete = withHeadless({
1515
setup({ props, id }) {
1616
const onListboxValueChange = React.useRef<((event: useListboxValueChangeEvent) => void) | null>(null);
1717

18+
const handleListboxValueChange = (event: useListboxValueChangeEvent) => {
19+
onListboxValueChange.current?.(event);
20+
};
21+
22+
const handleOpenChange = (event: usePopoverOpenChangeEvent) => {
23+
props.onOpenChange?.({ value: event.open });
24+
};
25+
1826
const listbox = useListbox({
1927
value: props.value,
2028
defaultValue: props.defaultValue,
@@ -32,7 +40,7 @@ export const useAutoComplete = withHeadless({
3240
focusOnHover: props.focusOnHover,
3341
multiple: false,
3442
metaKeySelection: props.metaKeySelection,
35-
onValueChange: (event: useListboxValueChangeEvent) => onListboxValueChange.current?.(event)
43+
onValueChange: handleListboxValueChange
3644
});
3745

3846
const popover = usePopover({
@@ -41,9 +49,7 @@ export const useAutoComplete = withHeadless({
4149
closeOnEscape: props.closeOnEscape,
4250
autoFocus: props.autoFocus,
4351
trapped: props.trapped,
44-
onOpenChange: (event: usePopoverOpenChangeEvent) => {
45-
props.onOpenChange?.({ value: event.open });
46-
}
52+
onOpenChange: handleOpenChange
4753
});
4854

4955
const [valueState, setValueState] = useControlledState({
@@ -168,12 +174,13 @@ export const useAutoComplete = withHeadless({
168174
search(event, (event.target as HTMLInputElement).value, 'focus');
169175
}
170176

177+
listbox.listProps.onFocus?.();
171178
setFocusedState(true);
172179
};
173180

174181
const onBlur = (event: React.FocusEvent) => {
182+
listbox.listProps.onBlur?.();
175183
setFocusedState(false);
176-
listbox.changeFocusedOptionIndex(new Event('blur') as unknown as React.KeyboardEvent, -1);
177184

178185
if (props.forceSelection) {
179186
applyForceSelection(event);

packages/@primereact/headless/src/badge/useBadge.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { defaultProps } from './useBadge.props';
44
export const useBadge = withHeadless({
55
name: 'useBadge',
66
defaultProps,
7-
setup() {
7+
setup({ props }) {
88
// prop getters
99
const rootProps = {
10-
'data-scope': 'badge',
11-
'data-part': 'root'
10+
'data-scope': 'badge' as const,
11+
'data-part': 'root' as const,
12+
...(!props.children && { 'data-empty': '' as const })
1213
};
1314

1415
return {

packages/@primereact/headless/src/breadcrumb/useBreadcrumb.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ export const useBreadcrumb = withHeadless({
55
name: 'useBreadcrumb',
66
defaultProps,
77
setup() {
8-
return {};
8+
// prop getters
9+
const rootProps = {
10+
'aria-label': 'Breadcrumb'
11+
};
12+
13+
return {
14+
rootProps
15+
};
916
}
1017
});

0 commit comments

Comments
 (0)