From b40f000dd5381f48cf05d021e036e03566caa446 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:33:57 +0200 Subject: [PATCH] fix(board): make container headers fully clickable (#7006) --- apps/docs/docs/management/boards/index.mdx | 3 +- .../board/sections/container-section.tsx | 33 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 7d6267687..b71d31c8e 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -50,7 +50,8 @@ was introduced; widen the affected Container before moving widgets into its righ ### Containers and rails A **Container** groups apps, widgets, and nested Containers. It can have a label, collapse control, app-opening action, -border color, and CSS classes. +border color, and CSS classes. Click anywhere on a collapsible Container’s header to collapse or expand it. +The header also supports Enter and Space; its app-opening action and edit controls work independently. Items can occupy the outermost rows and columns of a Container. The drop preview shows the position where the item will remain when released. diff --git a/apps/nextjs/src/components/board/sections/container-section.tsx b/apps/nextjs/src/components/board/sections/container-section.tsx index d18190329..9a93a9f32 100644 --- a/apps/nextjs/src/components/board/sections/container-section.tsx +++ b/apps/nextjs/src/components/board/sections/container-section.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from "react"; +import { createContext, useContext, useRef } from "react"; import dynamic from "next/dynamic"; import { ActionIcon, Badge, Box, Button, Card } from "@mantine/core"; import { IconChevronDown, IconChevronUp, IconExternalLink } from "@tabler/icons-react"; @@ -33,6 +33,7 @@ interface Props { export const BoardContainerSection = ({ section }: Props) => { const board = useRequiredBoard(); + const togglePointer = useRef<{ x: number; y: number; moved: boolean } | null>(null); const containerDepth = useContext(ContainerDepthContext); // Stagger nested controls inward and downward without wrapping back over ancestors. const menuRightOffset = 4 + containerDepth * 32; @@ -73,15 +74,15 @@ export const BoardContainerSection = ({ section }: Props) => { labelMaxWidth = `calc(100% - ${labelLeft}px - ${reservedMenuWidth})`; collapsedLabelPaddingRight = reservedMenuWidth; } - // Expanded controls sit on the border without reserving a header row. + // The native toggle spans the header; sibling actions stay above it. const toggleLayout = isVisuallyCollapsed ? { top: 0, left: 0, w: "100%", h: "100%", maw: "100%" } : { top: labelTop, - left: labelLeft, - w: "auto", + left: 0, + w: "100%", h: 20, - maw: labelMaxWidth, + maw: "100%", }; const toggleIcon = isVisuallyCollapsed ? ( @@ -125,13 +126,29 @@ export const BoardContainerSection = ({ section }: Props) => { pos="absolute" {...toggleLayout} px={6} - ps={isVisuallyCollapsed ? labelLeft : 6} - pe={isVisuallyCollapsed ? collapsedLabelPaddingRight : 6} + ps={labelLeft} + pe={collapsedLabelPaddingRight} radius="sm" variant="default" justify={options.showLabel ? "flex-start" : "center"} leftSection={options.showLabel && toggleIcon} - onClick={toggle} + onPointerDown={(event) => { + togglePointer.current = { x: event.clientX, y: event.clientY, moved: false }; + }} + onPointerMove={(event) => { + const pointer = togglePointer.current; + if (!pointer || event.buttons === 0) return; + if (Math.hypot(event.clientX - pointer.x, event.clientY - pointer.y) > 5) { + pointer.moved = true; + } + }} + onPointerCancel={() => { + togglePointer.current = null; + }} + onClick={(event) => { + if (event.detail > 0 && togglePointer.current?.moved) return; + toggle(); + }} aria-expanded={!isVisuallyCollapsed} aria-controls={contentId} aria-label={`${t(isVisuallyCollapsed ? "action.expand" : "action.collapse")}: ${label}`}