feat(task-list-v2): enhance sticky column behavior and dark mode support

- Updated DropSpacer and EmptyGroupMessage components to accept an optional isDarkMode prop for improved styling in dark mode.
- Enhanced task rendering in TaskRow to dynamically adjust background colors based on dark mode and drag states.
- Refactored useTaskRowColumns to support sticky column positioning and hover effects, ensuring a consistent user experience across different themes.
- Improved overall visual feedback during task interactions, including drag-and-drop operations.
This commit is contained in:
chamikaJ
2025-07-30 16:25:29 +05:30
parent b6c056dd1a
commit 374595261f
3 changed files with 167 additions and 35 deletions

View File

@@ -69,7 +69,11 @@ import ConvertToSubtaskDrawer from '@/components/task-list-common/convert-to-sub
import EmptyListPlaceholder from '@/components/EmptyListPlaceholder'; import EmptyListPlaceholder from '@/components/EmptyListPlaceholder';
// Drop Spacer Component - creates space between tasks when dragging // Drop Spacer Component - creates space between tasks when dragging
const DropSpacer: React.FC<{ isVisible: boolean; visibleColumns: any[] }> = ({ isVisible, visibleColumns }) => { const DropSpacer: React.FC<{ isVisible: boolean; visibleColumns: any[]; isDarkMode?: boolean }> = ({
isVisible,
visibleColumns,
isDarkMode = false
}) => {
if (!isVisible) return null; if (!isVisible) return null;
return ( return (
@@ -83,17 +87,34 @@ const DropSpacer: React.FC<{ isVisible: boolean; visibleColumns: any[] }> = ({ i
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
{visibleColumns.map((column) => { {visibleColumns.map((column, index) => {
// Calculate left position for sticky columns
let leftPosition = 0;
if (column.isSticky) {
for (let i = 0; i < index; i++) {
const prevColumn = visibleColumns[i];
if (prevColumn.isSticky) {
leftPosition += parseInt(prevColumn.width.replace('px', ''));
}
}
}
const columnStyle = { const columnStyle = {
width: column.width, width: column.width,
flexShrink: 0, flexShrink: 0,
...(column.isSticky && {
position: 'sticky' as const,
left: leftPosition,
zIndex: 5,
backgroundColor: 'inherit', // Inherit from parent spacer
}),
}; };
if (column.id === 'title') { if (column.id === 'title') {
return ( return (
<div <div
key={`spacer-${column.id}`} key={`spacer-${column.id}`}
className="flex items-center pl-1" className="flex items-center pl-1 border-r border-blue-300 dark:border-blue-600"
style={columnStyle} style={columnStyle}
> >
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium"> <span className="text-xs text-blue-600 dark:text-blue-400 font-medium">
@@ -116,13 +137,33 @@ const DropSpacer: React.FC<{ isVisible: boolean; visibleColumns: any[] }> = ({ i
}; };
// Empty Group Message Component // Empty Group Message Component
const EmptyGroupMessage: React.FC<{ visibleColumns: any[] }> = ({ visibleColumns }) => { const EmptyGroupMessage: React.FC<{ visibleColumns: any[]; isDarkMode?: boolean }> = ({
visibleColumns,
isDarkMode = false
}) => {
return ( return (
<div className="flex items-center min-w-max px-1 border-b border-gray-200 dark:border-gray-700" style={{ height: '40px' }}> <div className="flex items-center min-w-max px-1 border-b border-gray-200 dark:border-gray-700" style={{ height: '40px' }}>
{visibleColumns.map((column) => { {visibleColumns.map((column, index) => {
// Calculate left position for sticky columns
let leftPosition = 0;
if (column.isSticky) {
for (let i = 0; i < index; i++) {
const prevColumn = visibleColumns[i];
if (prevColumn.isSticky) {
leftPosition += parseInt(prevColumn.width.replace('px', ''));
}
}
}
const emptyColumnStyle = { const emptyColumnStyle = {
width: column.width, width: column.width,
flexShrink: 0, flexShrink: 0,
...(column.isSticky && {
position: 'sticky' as const,
left: leftPosition,
zIndex: 5,
backgroundColor: 'inherit', // Inherit from parent container
}),
}; };
// Show text in the title column // Show text in the title column
@@ -130,7 +171,7 @@ const EmptyGroupMessage: React.FC<{ visibleColumns: any[] }> = ({ visibleColumns
return ( return (
<div <div
key={`empty-${column.id}`} key={`empty-${column.id}`}
className="flex items-center pl-1" className="flex items-center pl-1 border-r border-gray-200 dark:border-gray-700"
style={emptyColumnStyle} style={emptyColumnStyle}
> >
<span className="text-sm text-gray-500 dark:text-gray-400 italic"> <span className="text-sm text-gray-500 dark:text-gray-400 italic">
@@ -550,7 +591,7 @@ const TaskListV2Section: React.FC = () => {
projectId={urlProjectId || ''} projectId={urlProjectId || ''}
/> />
{isGroupEmpty && !isGroupCollapsed && ( {isGroupEmpty && !isGroupCollapsed && (
<EmptyGroupMessage visibleColumns={visibleColumns} /> <EmptyGroupMessage visibleColumns={visibleColumns} isDarkMode={isDarkMode} />
)} )}
</div> </div>
); );
@@ -596,19 +637,40 @@ const TaskListV2Section: React.FC = () => {
const renderColumnHeaders = useCallback( const renderColumnHeaders = useCallback(
() => ( () => (
<div <div
className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700" className="border-b border-gray-200 dark:border-gray-700"
style={{ width: '100%', minWidth: 'max-content' }} style={{
width: '100%',
minWidth: 'max-content',
backgroundColor: isDarkMode ? '#141414' : '#f9fafb'
}}
> >
<div <div
className="flex items-center px-1 py-3 w-full" className="flex items-center px-1 py-3 w-full"
style={{ minWidth: 'max-content', height: '44px' }} style={{ minWidth: 'max-content', height: '44px' }}
> >
{visibleColumns.map((column, index) => { {visibleColumns.map((column, index) => {
// Calculate left position for sticky columns
let leftPosition = 0;
if (column.isSticky) {
for (let i = 0; i < index; i++) {
const prevColumn = visibleColumns[i];
if (prevColumn.isSticky) {
leftPosition += parseInt(prevColumn.width.replace('px', ''));
}
}
}
const columnStyle: ColumnStyle = { const columnStyle: ColumnStyle = {
width: column.width, width: column.width,
flexShrink: 0, flexShrink: 0,
...((column as any).minWidth && { minWidth: (column as any).minWidth }), ...((column as any).minWidth && { minWidth: (column as any).minWidth }),
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }), ...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
...(column.isSticky && {
position: 'sticky' as const,
left: leftPosition,
zIndex: 10,
backgroundColor: isDarkMode ? '#141414' : '#f9fafb', // custom dark header : bg-gray-50
}),
}; };
return ( return (
@@ -773,14 +835,25 @@ const TaskListV2Section: React.FC = () => {
} }
return ( return (
<DndContext <>
sensors={sensors} {/* CSS for sticky column hover effects */}
collisionDetection={closestCenter} <style>
onDragStart={handleDragStart} {`
onDragOver={handleDragOver} .hover\\:bg-gray-50:hover .sticky-column-hover,
onDragEnd={handleDragEnd} .dark .hover\\:bg-gray-800:hover .sticky-column-hover {
modifiers={[restrictToVerticalAxis]} background-color: var(--hover-bg) !important;
> }
`}
</style>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis]}
>
<div className="flex flex-col bg-white dark:bg-gray-900 h-full overflow-hidden"> <div className="flex flex-col bg-white dark:bg-gray-900 h-full overflow-hidden">
{/* Table Container */} {/* Table Container */}
<div <div
@@ -841,9 +914,9 @@ const TaskListV2Section: React.FC = () => {
return ( return (
<div key={task.id || `add-task-${group.id}-${taskIndex}`}> <div key={task.id || `add-task-${group.id}-${taskIndex}`}>
{showDropSpacerBefore && <DropSpacer isVisible={true} visibleColumns={visibleColumns} />} {showDropSpacerBefore && <DropSpacer isVisible={true} visibleColumns={visibleColumns} isDarkMode={isDarkMode} />}
{renderTask(globalTaskIndex, isFirstTaskInGroup)} {renderTask(globalTaskIndex, isFirstTaskInGroup)}
{showDropSpacerAfter && <DropSpacer isVisible={true} visibleColumns={visibleColumns} />} {showDropSpacerAfter && <DropSpacer isVisible={true} visibleColumns={visibleColumns} isDarkMode={isDarkMode} />}
</div> </div>
); );
}) })
@@ -917,6 +990,7 @@ const TaskListV2Section: React.FC = () => {
{createPortal(<ConvertToSubtaskDrawer />, document.body, 'convert-to-subtask-drawer')} {createPortal(<ConvertToSubtaskDrawer />, document.body, 'convert-to-subtask-drawer')}
</div> </div>
</DndContext> </DndContext>
</>
); );
}; };

View File

@@ -131,11 +131,26 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
isOver && !isDragging ? 'bg-blue-50 dark:bg-blue-900/20' : '' isOver && !isDragging ? 'bg-blue-50 dark:bg-blue-900/20' : ''
}`} }`}
> >
{visibleColumns.map((column, index) => ( {visibleColumns.map((column, index) => {
<React.Fragment key={column.id}> // Calculate background state for sticky columns - custom dark mode colors
{renderColumn(column.id, column.width, column.isSticky, index)} const rowBackgrounds = {
</React.Fragment> normal: isDarkMode ? '#1e1e1e' : '#ffffff', // custom dark : bg-white
))} hover: isDarkMode ? '#1f2937' : '#f9fafb', // slightly lighter dark : bg-gray-50
dragOver: isDarkMode ? '#1e3a8a33' : '#dbeafe', // bg-blue-900/20 : bg-blue-50
};
let currentBg = rowBackgrounds.normal;
if (isOver && !isDragging) {
currentBg = rowBackgrounds.dragOver;
}
// Note: hover state is handled by CSS, so we'll use a CSS custom property
return (
<React.Fragment key={column.id}>
{renderColumn(column.id, column.width, column.isSticky, index, currentBg, rowBackgrounds)}
</React.Fragment>
);
})}
</div> </div>
); );
}); });

View File

@@ -90,17 +90,39 @@ export const useTaskRowColumns = ({
depth = 0, depth = 0,
}: UseTaskRowColumnsProps) => { }: UseTaskRowColumnsProps) => {
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number, currentBg?: string, rowBackgrounds?: any) => {
switch (columnId) { // Calculate left position for sticky columns
case 'dragHandle': let leftPosition = 0;
return ( if (isSticky && typeof index === 'number') {
<DragHandleColumn for (let i = 0; i < index; i++) {
width={width} const prevColumn = visibleColumns[i];
isSubtask={isSubtask} if (prevColumn.isSticky) {
attributes={attributes} leftPosition += parseInt(prevColumn.width.replace('px', ''));
listeners={listeners} }
/> }
); }
// Create wrapper style for sticky positioning
const wrapperStyle = isSticky ? {
position: 'sticky' as const,
left: leftPosition,
zIndex: 5, // Lower than header but above regular content
backgroundColor: currentBg || (isDarkMode ? '#1e1e1e' : '#ffffff'), // Use dynamic background or fallback
overflow: 'hidden', // Prevent content from spilling over
width: width, // Ensure the wrapper respects column width
} : {};
const renderColumnContent = () => {
switch (columnId) {
case 'dragHandle':
return (
<DragHandleColumn
width={width}
isSubtask={isSubtask}
attributes={attributes}
listeners={listeners}
/>
);
case 'checkbox': case 'checkbox':
return ( return (
@@ -294,7 +316,27 @@ export const useTaskRowColumns = ({
); );
} }
return null; return null;
}
};
// Wrap content with sticky positioning if needed
const content = renderColumnContent();
if (isSticky) {
const hoverBg = rowBackgrounds?.hover || (isDarkMode ? '#2a2a2a' : '#f9fafb');
return (
<div
style={{
...wrapperStyle,
'--hover-bg': hoverBg,
} as React.CSSProperties}
className="border-r border-gray-200 dark:border-gray-700 overflow-hidden sticky-column-hover hover:bg-[var(--hover-bg)]"
>
{content}
</div>
);
} }
return content;
}, [ }, [
task, task,
projectId, projectId,
@@ -319,6 +361,7 @@ export const useTaskRowColumns = ({
handleTaskNameEdit, handleTaskNameEdit,
attributes, attributes,
listeners, listeners,
depth,
]); ]);
return { renderColumn }; return { renderColumn };