feat: tree grouping for list and cards views (matching table view)
This commit is contained in:
@@ -475,10 +475,48 @@ export function ContactList({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Group header renderer for list view
|
||||||
|
const renderListGroupHeader = (group: GroupedContacts) => {
|
||||||
|
const isExpanded = expandedGroups.has(group.key);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`group-header-${group.key}`}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 bg-secondary-50 border-b border-secondary-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleGroup(group.key)}
|
||||||
|
className="flex items-center justify-center w-5 h-5 hover:bg-secondary-200 rounded transition-colors flex-shrink-0"
|
||||||
|
aria-label={isExpanded ? 'Collapse group' : 'Expand group'}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="font-medium text-sm text-secondary-700 truncate">{group.label}</span>
|
||||||
|
<span className="text-secondary-400 text-xs flex-shrink-0">({group.contacts.length})</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full" data-testid="contact-list-view">
|
<div className="flex flex-col h-full" data-testid="contact-list-view">
|
||||||
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||||
{shouldVirtualize ? (
|
{isGrouped && groupedContacts ? (
|
||||||
|
<div>
|
||||||
|
{groupedContacts.map((group) => (
|
||||||
|
<div key={group.key}>
|
||||||
|
{renderListGroupHeader(group)}
|
||||||
|
{expandedGroups.has(group.key) && (
|
||||||
|
<ul className="divide-y divide-secondary-100" role="list">
|
||||||
|
{group.contacts.map((contact) => renderContactItem(contact))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : shouldVirtualize ? (
|
||||||
<ul className="divide-y divide-secondary-100" role="list" style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
|
<ul className="divide-y divide-secondary-100" role="list" style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
const contact = contacts[virtualRow.index];
|
const contact = contacts[virtualRow.index];
|
||||||
@@ -826,10 +864,48 @@ export function ContactList({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Group header renderer for cards view
|
||||||
|
const renderCardsGroupHeader = (group: GroupedContacts) => {
|
||||||
|
const isExpanded = expandedGroups.has(group.key);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`group-header-${group.key}`}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 bg-secondary-50 border-b border-secondary-200 mb-3 sticky top-0 z-10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleGroup(group.key)}
|
||||||
|
className="flex items-center justify-center w-5 h-5 hover:bg-secondary-200 rounded transition-colors flex-shrink-0"
|
||||||
|
aria-label={isExpanded ? 'Collapse group' : 'Expand group'}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="font-medium text-sm text-secondary-700 truncate">{group.label}</span>
|
||||||
|
<span className="text-secondary-400 text-xs flex-shrink-0">({group.contacts.length})</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full" data-testid="contact-cards-view">
|
<div className="flex flex-col h-full" data-testid="contact-cards-view">
|
||||||
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3">
|
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3">
|
||||||
{shouldVirtualize ? (
|
{isGrouped && groupedContacts ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{groupedContacts.map((group) => (
|
||||||
|
<div key={group.key}>
|
||||||
|
{renderCardsGroupHeader(group)}
|
||||||
|
{expandedGroups.has(group.key) && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{group.contacts.map((contact) => renderContactCard(contact))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : shouldVirtualize ? (
|
||||||
<div style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
|
<div style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3" style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3" style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user