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:
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user