Files
microsoft-ui-xaml/controls/dev/TableView/TableViewContextMenu.h
T
Dipesh KumarandCopilot 09cfc8ff3f TableView: context menu support (cell, header, row and table flyouts) (#12306)
## Fixes

Brings the TableView context menu work (reviewed in #12299, merged into
`user/dipesh/feature/tableview-experimental`) into main.

## PR Type

- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## Description

Adds context menu support to TableView for cells, column headers, rows,
group headers and the table itself, from mouse, touch, keyboard
(Shift+F10 / Menu key) and UI Automation.

### Current Behavior
TableView has no per-column menus. A right-click only shows whatever
`ContextFlyout` the framework finds on the hit element, keyboard and UIA
requests have no defined target, and menus do not get the row's item as
their DataContext.

### New Behavior
- **New API (preview):** `TableViewColumn.CellContextFlyout` and
`TableViewColumn.HeaderContextFlyout` (+ DP properties).
- **Resolution, first match wins:**
- Cell: a `ContextFlyout` inside the cell content → `CellContextFlyout`
→ the row's `ContextFlyout` → `TableView.ContextFlyout`.
- Header: a `ContextFlyout` inside the `HeaderTemplate` →
`HeaderContextFlyout` → `TableView.ContextFlyout`.
- Rows and group headers get menus from implicit `TableViewRow` /
`TableViewGroupHeader` styles; a group header's DataContext is its
`TableViewGroupInfo`.
- Menu items inherit the requested row's item as DataContext, so one
shared menu works for every row.
- Opening a menu focuses the target cell/row and never changes
selection. Keyboard and UIA requests open at the focused cell.
- No TableView menu opens while a cell is editing. Nested TableViews /
items hosts and text controls' default flyouts are not picked up by the
outer table.
- Automation peers (row, cell, header, group header, table) implement
`ShowContextMenu`; pooled/collapsed elements open nothing.
- Editing: focus moving into a flyout the editor owns (attached flyout,
button flyout, picker) keeps the edit open, including while focus rests
on the flyout's `Popup`; focus in an unrelated popup commits.
- Sample app: new **Context menus** page; README updated.
- Tests: context menu API tests, interaction tests, editing focus tests,
shared interaction test helpers.

## Customer Impact

User-facing: apps can give TableView cells, headers and rows their own
context menus with no code-behind wiring. Additive, preview API.

## Regression Potential

- [ ] Low risk — isolated change, limited scope
- [x] Medium risk — touches shared components or public APIs
- [ ] High risk — architectural or breaking API change

New public properties on `TableViewColumn`. Touches row/cell focus on
context requests, editing focus-loss commit, and TableView automation
peers.

## How Has This Been Tested?

- [x] I have performed a self-review of my own code
- [x] I have added tests to cover my changes
- [x] Existing tests pass locally

Builds (x64chk, 0 errors):
- `.\initrun.ps1 .\build.cmd /q` — passed
- `.\initrun.ps1 msb /q /m
controls\test\MUXControlsTestApp\MUXControlsTestApp.csproj
/p:Platform=x64 /p:Configuration=Debug` — passed
- `.\initrun.ps1 msb /q /m
controls\test\MUXControls.Test\MUXControls.Test.csproj /p:Platform=x64
/p:Configuration=Debug` — passed
- `.\initrun.ps1 msb /q /restore
Samples\TableViewSampleApp\TableViewSampleApp.csproj /p:Platform=x64
/p:WinUIVersion=3.0.0-dev` — passed

TAEF on a test VM (115 tests: context menu API + interaction, editing
API, hierarchy interaction), run in 9 batches:
- `.\initrun.ps1 .\tools\run-tests-on-vm.ps1 -VMName winui-test2
-SkipPayload * -fromFile:batchN.txt` — 113/115 passed on the first run.
The 2 failures (`VerifyFocusInEditorOwnedFlyoutKeepsEditOpen`,
`PressAndHoldOnCellOpensCellMenu`) were fixed (product + test) and the
rerun passed 4/4 including neighbouring tests.

Runtime: sample app Context menus and Hierarchy pages navigated through
UIA after the fixes; app stays up and tables render.

## Screenshots (if appropriate)

N/A

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75efebd4-6de1-4fe8-b075-fab226b722c6
Copilot-Session: 128a3e81-37b9-46df-b080-757f110ae417
2026-10-20 15:35:44 +05:30

63 lines
2.2 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
#pragma once
#include "pch.h"
namespace TableViewDetails
{
enum class ContextMenuTargetKind
{
Body,
Header,
};
struct ContextMenuTarget
{
ContextMenuTargetKind Kind{ ContextMenuTargetKind::Body };
winrt::TableViewRow Row{ nullptr };
winrt::IInspectable Item{ nullptr };
winrt::TableViewColumn Column{ nullptr };
winrt::FrameworkElement Anchor{ nullptr };
winrt::Panel ScopeRoot{ nullptr };
};
struct ContextMenuCandidate
{
winrt::FlyoutBase Flyout{ nullptr };
winrt::FrameworkElement Anchor{ nullptr };
explicit operator bool() const noexcept { return static_cast<bool>(Flyout); }
};
// True when a header or row cell anchor is still the visible cell for column, directly under
// its cells host.
bool IsLiveCellAnchor(
winrt::FrameworkElement const& anchor,
winrt::Panel const& scopeRoot,
winrt::TableViewColumn const& column);
// True when a request's OriginalSource is scope or inside it without crossing a nested
// TableView or TableViewRow: a request from a nested table belongs to that table, even when it
// left it Unhandled.
bool IsOwnRequestSource(winrt::DependencyObject const& source, winrt::DependencyObject const& scope);
// Hooks ContextRequested on control's template root. The framework shows the control's own
// ContextFlyout before the control's handlers run, so suppression must sit below it.
template <typename Handler>
winrt::UIElement::ContextRequested_revoker HookTemplateRootContextRequested(
winrt::DependencyObject const& control,
Handler&& handler)
{
if (winrt::VisualTreeHelper::GetChildrenCount(control) > 0)
{
if (auto const templateRoot = winrt::VisualTreeHelper::GetChild(control, 0).try_as<winrt::UIElement>())
{
return templateRoot.ContextRequested(winrt::auto_revoke, std::forward<Handler>(handler));
}
}
return {};
}
}