From 21b6d17842f8716fb239a0087aaf07fb380a1fd0 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Aug 2026 14:00:24 +0200 Subject: [PATCH 1/3] TabViewItem: don't enter the drag visual state when dragging is disallowed BeginCheckingForDrag armed drag-checking on every pointer press, regardless of whether the parent TabView permits a drag gesture. With CanReorderTabs=false (and CanDragTabs defaulting to false), ListViewBase::GetIsDragEnabled() is false, so ListViewBaseItem never calls BeginCheckingForMouseDrag and never captures the pointer. The item therefore stops receiving pointer events once the pointer leaves its bounds and never sees the PointerReleased that would take it back out of DragDropVisualVisible, leaving the tab stuck in the drag visual state until it is unfocused and refocused. Only arm drag-checking when the framework will actually capture the pointer, mirroring ListViewBase::GetIsDragEnabled() plus the item's own CanDrag. CanTearOutTabs is deliberately not part of the condition: tear-out registers the tab strip as a Caption non-client region, so pointer events do not reach the item at all. Fixes #10749 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AEWdGmNFY5YbSAAf9xBmDi --- controls/dev/TabView/TabViewItem.cpp | 18 ++++++++++++++++++ controls/dev/TabView/TabViewItem.h | 1 + 2 files changed, 19 insertions(+) diff --git a/controls/dev/TabView/TabViewItem.cpp b/controls/dev/TabView/TabViewItem.cpp index 5c2479d298..de07e4b13e 100644 --- a/controls/dev/TabView/TabViewItem.cpp +++ b/controls/dev/TabView/TabViewItem.cpp @@ -485,8 +485,26 @@ bool TabViewItem::ShouldStartDrag(winrt::PointerRoutedEventArgs const& args) m_dragPointerId == args.Pointer().PointerId(); } +// Mirrors the conditions under which ListViewBaseItem captures the pointer to detect a drag gesture. +// Without that capture this item stops receiving pointer events once the pointer leaves its bounds, so it +// would never see the PointerReleased that takes it back out of the drag visual state. +bool TabViewItem::IsDraggingAllowed() +{ + if (const auto tabView = GetParentTabView()) + { + return tabView.CanDragTabs() || tabView.CanReorderTabs() || CanDrag(); + } + + return true; +} + void TabViewItem::BeginCheckingForDrag(uint32_t const& pointerId) { + if (!IsDraggingAllowed()) + { + return; + } + m_dragPointerId = pointerId; m_isCheckingforDrag = true; } diff --git a/controls/dev/TabView/TabViewItem.h b/controls/dev/TabView/TabViewItem.h index 13c6bf9022..81e9680eff 100644 --- a/controls/dev/TabView/TabViewItem.h +++ b/controls/dev/TabView/TabViewItem.h @@ -82,6 +82,7 @@ class TabViewItem : void UpdateSelectedBackgroundPathTranslateTransform(); + bool IsDraggingAllowed(); void BeginCheckingForDrag(const uint32_t& pointerId); void StopCheckingForDrag(const uint32_t& pointerId); From 39327275de97c445afd1e6d1b0f3f8a30b0a6879 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Aug 2026 14:00:41 +0200 Subject: [PATCH 2/3] TabViewItem: add interaction tests for the drag visual state Covers both directions of the drag/drop visual state: - VerifyTabEntersDragVisualStateWhenDraggingIsEnabled: dragging a tab still shows the drag visual when the TabView allows dragging. - VerifyTabDoesNotEnterDragVisualStateWhenDraggingIsDisabled: with both CanReorderTabs and CanDragTabs off, dragging a tab away and releasing the pointer outside it neither enters DragDropVisualVisible nor leaves the tab stuck in it. TabViewPage gains CanDragTabs/CanReorderTabs checkboxes and a readout that records every transition of the drag/drop visual state group on FirstTab. The group is unnamed in the TabViewItem template, so it is located by the states it contains. Recording the transitions (rather than only sampling the final state) is what lets the disabled case assert that the visual state is never entered, not merely that it was left correctly. Verified against a local x64chk build: the second test fails without the TabViewItem fix ("Visited states: 'DragDropVisualVisible'", with no return to DragDropVisualNotVisible) and passes with it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AEWdGmNFY5YbSAAf9xBmDi --- .../TabView/InteractionTests/TabViewTests.cs | 50 ++++++++++++++++++ controls/dev/TabView/TestUI/TabViewPage.xaml | 11 ++++ .../dev/TabView/TestUI/TabViewPage.xaml.cs | 51 +++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/controls/dev/TabView/InteractionTests/TabViewTests.cs b/controls/dev/TabView/InteractionTests/TabViewTests.cs index f3e0d28d38..d70ec7c18c 100644 --- a/controls/dev/TabView/InteractionTests/TabViewTests.cs +++ b/controls/dev/TabView/InteractionTests/TabViewTests.cs @@ -781,6 +781,56 @@ public void VerifyDragStartedCalledOnItemDrag() } } + [TestMethod] + [TestProperty("Description", "Verifies that dragging a tab shows the drag visual when the TabView allows dragging.")] + public void VerifyTabEntersDragVisualStateWhenDraggingIsEnabled() + { + using (var setup = new TestSetupHelper("TabView Tests")) + { + FindElement.ByName