-
Notifications
You must be signed in to change notification settings - Fork 5.6k
[WEB-9677] feat: allow resizing Kanban columns #9691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,12 +19,19 @@ import type { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TIssueOrderByOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@plane/types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // constants | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STATE_GROUP_COLUMN_DEFAULT_WIDTH, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STATE_GROUP_COLUMN_MAX_WIDTH, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STATE_GROUP_COLUMN_MIN_WIDTH, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@plane/constants"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ContentWrapper } from "@plane/ui"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from "@plane/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // components | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import RenderIfVisible from "@/components/core/render-if-visible-HOC"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { KanbanColumnLoader } from "@/components/ui/loader/layouts/kanban-layout-loader"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // hooks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useKanbanView } from "@/hooks/store/use-kanban-view"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useKanbanColumnResize } from "@/hooks/use-kanban-column-resize"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // types | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // parent components | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -56,6 +63,8 @@ export interface IKanBan { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quickActions: TRenderQuickActions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collapsedGroups: TIssueKanbanFilters; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupWidths?: Record<string, number>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onResizeColumnWidth?: (columnId: string, width: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadMoreIssues: (groupId?: string, subGroupId?: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enableQuickIssueCreate?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -82,6 +91,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quickActions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collapsedGroups, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleCollapsedGroups, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupWidths = {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onResizeColumnWidth, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enableQuickIssueCreate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quickAddCallback, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadMoreIssues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -142,6 +153,16 @@ export const KanBan = observer(function KanBan(props: IKanBan) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isGroupByCreatedBy = group_by === "created_by"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const approximateCardHeight = getApproximateCardHeight(displayProperties); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isSubGroup = !!sub_group_id && sub_group_id !== "null"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { isResizing, resizingColumnId, currentWidth, startResize } = useKanbanColumnResize({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| minWidth: STATE_GROUP_COLUMN_MIN_WIDTH, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxWidth: STATE_GROUP_COLUMN_MAX_WIDTH, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onResizeEnd: onResizeColumnWidth ?? (() => {}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getEffectiveWidth = (columnId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isResizing && resizingColumnId === columnId) return currentWidth; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return groupWidths[columnId] ?? STATE_GROUP_COLUMN_DEFAULT_WIDTH; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ContentWrapper className={`relative flex-row gap-4 !pt-2 !pb-0`}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -161,9 +182,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={subList.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`group relative flex flex-shrink-0 flex-col ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupByVisibilityToggle.showIssues ? `w-[350px]` : `` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } `} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="group relative flex flex-shrink-0 flex-col" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={groupByVisibilityToggle.showIssues ? { width: `${getEffectiveWidth(subList.id)}px` } : undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {sub_group_by === null && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-surface-2 py-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -231,6 +251,19 @@ export const KanBan = observer(function KanBan(props: IKanBan) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </RenderIfVisible> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!isSubGroup && groupByVisibilityToggle.showIssues && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="separator" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Resize column" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "absolute top-0 right-0 z-[3] h-full w-2 cursor-ew-resize transition-colors", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "after:bg-subtle after:absolute after:top-0 after:right-1/2 after:h-full after:w-px after:translate-x-1/2", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isResizing && resizingColumnId === subList.id ? "bg-accent-primary/20" : "hover:bg-surface-2" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMouseDown={(e) => startResize(e, subList.id, getEffectiveWidth(subList.id))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+255
to
+265
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Provide keyboard operation for the resize separator. The Proposed change <div
role="separator"
aria-label="Resize column"
+ aria-orientation="vertical"
+ aria-valuemin={STATE_GROUP_COLUMN_MIN_WIDTH}
+ aria-valuemax={STATE_GROUP_COLUMN_MAX_WIDTH}
+ aria-valuenow={Math.round(getEffectiveWidth(subList.id))}
+ tabIndex={0}
className={cn(
"absolute top-0 right-0 z-[3] h-full w-2 cursor-ew-resize transition-colors",
"after:bg-subtle after:absolute after:top-0 after:right-1/2 after:h-full after:w-px after:translate-x-1/2",
- isResizing && resizingColumnId === subList.id ? "bg-accent-primary/20" : "hover:bg-surface-2"
+ isResizing && resizingColumnId === subList.id
+ ? "bg-accent-primary/20"
+ : "hover:bg-surface-2 focus-visible:bg-surface-2"
)}
onMouseDown={(e) => startResize(e, subList.id, getEffectiveWidth(subList.id))}
+ onKeyDown={(e) => {
+ const delta = e.key === "ArrowLeft" ? -10 : e.key === "ArrowRight" ? 10 : 0;
+ if (!delta) return;
+ e.preventDefault();
+ onResizeColumnWidth?.(
+ subList.id,
+ Math.min(
+ Math.max(getEffectiveWidth(subList.id) + delta, STATE_GROUP_COLUMN_MIN_WIDTH),
+ STATE_GROUP_COLUMN_MAX_WIDTH
+ )
+ );
+ }}
/>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /** | ||
| * Copyright (c) 2023-present Plane Software, Inc. and contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-only | ||
| * See the LICENSE file for details. | ||
| */ | ||
|
|
||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||
|
|
||
| interface IUseKanbanColumnResize { | ||
| minWidth: number; | ||
| maxWidth: number; | ||
| onResizeEnd: (columnId: string, width: number) => void; | ||
| } | ||
|
|
||
| export const useKanbanColumnResize = ({ minWidth, maxWidth, onResizeEnd }: IUseKanbanColumnResize) => { | ||
| // state | ||
| const [resizingColumnId, setResizingColumnId] = useState<string | null>(null); | ||
| const [currentWidth, setCurrentWidth] = useState<number>(minWidth); | ||
| // refs | ||
| const startXRef = useRef(0); | ||
| const startWidthRef = useRef(minWidth); | ||
| const currentWidthRef = useRef(minWidth); | ||
| const onResizeEndRef = useRef(onResizeEnd); | ||
| onResizeEndRef.current = onResizeEnd; | ||
|
|
||
| const startResize = useCallback((e: React.MouseEvent, columnId: string, startWidth: number) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| startXRef.current = e.clientX; | ||
| startWidthRef.current = startWidth; | ||
| currentWidthRef.current = startWidth; | ||
| setCurrentWidth(startWidth); | ||
| setResizingColumnId(columnId); | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| if (!resizingColumnId) return; | ||
|
|
||
| const handleMouseMove = (e: MouseEvent) => { | ||
| const deltaX = e.clientX - startXRef.current; | ||
| const nextWidth = Math.min(Math.max(startWidthRef.current + deltaX, minWidth), maxWidth); | ||
| currentWidthRef.current = nextWidth; | ||
| setCurrentWidth(nextWidth); | ||
| }; | ||
|
|
||
| const handleMouseUp = () => { | ||
| onResizeEndRef.current(resizingColumnId, currentWidthRef.current); | ||
| setResizingColumnId(null); | ||
| }; | ||
|
|
||
| document.addEventListener("mousemove", handleMouseMove); | ||
| document.addEventListener("mouseup", handleMouseUp); | ||
| document.body.style.cursor = "col-resize"; | ||
| document.body.style.userSelect = "none"; | ||
|
|
||
| return () => { | ||
| document.removeEventListener("mousemove", handleMouseMove); | ||
| document.removeEventListener("mouseup", handleMouseUp); | ||
| document.body.style.cursor = ""; | ||
| document.body.style.userSelect = ""; | ||
| }; | ||
| }, [resizingColumnId, minWidth, maxWidth]); | ||
|
|
||
| return { | ||
| isResizing: resizingColumnId !== null, | ||
| resizingColumnId, | ||
| currentWidth, | ||
| startResize, | ||
| }; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.