Commit Graph
41 Commits
Author SHA1 Message Date
Saharsh JaiswalandCopilot 42b575bc75 Merge main into user/sahjaiswal/tableview-tip-followup
Brings in #12307 (TableView: templated column headers, UIA resize, sort
fixes, recycle perf, theming) and 11 other commits from main.

Conflicts, resolved by keeping both sides:
- TableView_Editing.cpp (BeginEdit): the TiP try/catch now wraps #12307's
  unwind-on-throw guard and ResetPendingBeginEdit.
- TableView_Sort.cpp: the typed Sorted rethrow and failure capture keep
  #12307's explicitClear, keptSourceSort and reconcile changes, and the
  comparer tally reset follows its early return for a source another
  table owns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-23 15:03:50 +05:30
Hitesh KumarandCopilot d5132fd2e4 TableView: templated column headers, UIA resize, sort fixes, recycle perf, theming (#12307)
## Fixes

| Internal issue | Type | Change |
|---|---|---|
| https://task.ms/64475625 | Bug | Column headers are a templated,
restylable `TableViewColumnHeader` with visual states |
| https://task.ms/64475623 | Bug | Header typography comes from the
header Style, not C++ local values |
| https://task.ms/64143451 | Bug | Header column separators are always
drawn (no new property); hide them by overriding
`TabularSurfaceHeaderSeparatorBrush` |
| https://task.ms/63996584 | Task | Column headers expose the UIA
Transform pattern so UIA clients can resize columns |
| https://task.ms/64475611 | Bug | Outer border uses new
`TabularSurfaceOuterBorderBrush` (`CardStrokeColorDefault`); grid-line
values were in #12349 |
| https://task.ms/64475629 | Bug | Default group header uses theme
tokens (SemiBold key style, 8px spacing); the item count inherits the
header `Foreground` |
| https://task.ms/64652961 | Task | Row recycling no longer regenerates
cells or refreshes row chrome twice (scroll performance) |
| https://task.ms/63790855 | Task | Sort: `CanSort` is enforced and
`SortByColumn(column, None)` always clears; a sort through a source
another table owns returns false; no spurious `Sorted`; an app-owned
sort survives a `CanSort`/`CanUserSortColumns` opt-out |
| https://task.ms/63787021 | Task | Cell automation-peer constructor
validates its arguments; `BeginEdit` rolls back when it throws |

Not changed: https://task.ms/64512732 (by design: Auto columns follow
content width), https://task.ms/64475627 (design decision: keep chevron
sort glyphs), https://task.ms/64513223 (investigation only; fix
deferred).

## PR Type

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

## Description

Follow-up to #12349 (merged) and #12285.

Column headers move from code-built Grids to a new templated
`TableViewColumnHeader`, restylable through
`TableView.ColumnHeaderStyle` or `TableViewColumn.HeaderStyle`. Header
peers gain the UIA Transform pattern for column resize. Sort behavior is
tightened around `CanSort`, opt-outs and shared `TableViewSource`
ownership. Row recycling reuses cells. Outer border, header lines and
the default group header follow Fluent tokens.

Review-driven fixes included here:
- The header Style's `TargetType` `TypeName` is kept alive while it is
compared; a Style with only `BasedOn` typed is applied, and its setters
are read before applying it seals the Style. Incompatible, untyped or
cyclic `BasedOn` Styles are ignored with a debug diagnostic.
- A header disabled by its Style reports `IsEnabled=false` and is not
invokable through UIA.
- UIA `Transform.Resize` converts physical pixels through the table's
full visual scale (not only `RasterizationScale`), and a width change
below the drag jitter deadband is written once, exactly.
- Disabling a header in its High Contrast hover/pressed state keeps the
disabled foreground (state order fixed); the disabled sort glyph follows
the text brush across theme changes.
- A `CanSort` opt-out made inside `BeginningEdit` is replayed after the
edit it let open closes, and rebuilds headers even when a `Sorted`
handler throws.
- Header rebuilds caused by `Content`/`Header` or `Columns` changes keep
focus in the header band, respect an app's own focus move, and release
`UIElement` headers back to the app.
- A disabled group header does not dim its item count twice. The count
opacity follows `ContentTemplate` changes while the header is disabled.

<details><summary>Product changes by file</summary>

| File | +/- | What changed | Items |
|---|---|---|---|
| `TableView/TableView.cpp` (+ .h) | +684 / −382 | `RebuildHeaders`
creates `TableViewColumnHeader` controls; applies
`ColumnHeaderStyle`/`HeaderStyle` with TargetType/BasedOn checks;
density MinHeight/Padding only when the Style doesn't set
them<br>`ReleaseHeaderCells` and header-focus handling for
removed/replaced columns, re-entrant rebuilds and app focus
moves<br>Header bottom rule thickness set on `PART_HeaderRow`; header
lines no longer follow `GridLinesVisibility`<br>Row prepare/clear
refresh chrome once; `TryResizeColumnFromAutomation` writes small widths
exactly | 64475625, 64475623, 64143451, 64652961, 63996584, 63790855 |
| `TableView/TableViewColumnHeader.cpp` (+ .h; replaces
`TableViewHeaderCell.h`) | +435 / −210 | New sealed templated
`ContentControl` with read-only `Column`<br>Resolves
`PART_ContentPresenter`, `PART_GridLine`, `PART_SortIndicatorHost`,
`PART_ResizeGripperHost`; hosts the sort glyph and gripper<br>Drives
`CommonStates`/`HighContrastStates`/`SortStates`; disabled sort glyph
bound to the text brush | 64475625, 64475623 |
| `TableView/TableViewRow.cpp` (+ .h) | +191 / −35 | Recycled rows keep
their cells unless cell automation peers exist; chrome refreshed once;
tooltips re-applied on recycle<br>`BeginCellEdit` restores display
element, edit state and focus on throw | 64652961, 63787021 |
| `TableView/TableView.xaml` | +128 / −18 | Default
`TableViewColumnHeader` Style/template: typography setters, visual
states, parts<br>Outer border uses `TabularSurfaceOuterBorderBrush`;
header lines use `TabularSurfaceHeaderSeparatorBrush`<br>Group header:
`TableViewGroupExpanderSpacing` (8), padding 8,8,12,8, key/count text
styles, count inherits `Foreground` | 64475625, 64475623, 64475611,
64475629 |
| `TableView/TableViewColumnHeaderAutomationPeer.cpp` (+ .h) | +127 / −9
| `ITransformProvider`: `Resize` converts physical px through the visual
scale; `CanResize` tracks gripper availability (pinned →
false)<br>`IsEnabled` follows the header; name read from the content
presenter | 63996584, 64475625 |
| `TableView/TableView_Sort.cpp` | +113 / −39 | `SortByColumn(None)`
bypasses the `CanSort` gate; other sorts refused<br>Sort through a
source owned by another table returns false; implicit clears keep the
app's sort and raise no `Sorted`; app-lit columns left alone on opt-out
| 63790855 |
| `TableView/TableView_Layout.cpp` | +74 / −14 | Realized-row walk only
when a visible Auto column exists<br>Transform `CanResize` changes
raised for existing header peers | 64652961, 63996584 |
| `TableView/TableView_Editing.cpp` | +63 / −15 | `BeginEdit` unwinds on
throw; `CanSort` opt-out during `BeginningEdit` queued until the edit
closes | 63787021, 63790855 |
| `TableView/TableViewCellAutomationPeer.cpp` (+ .h, `TableViewCell.h`)
| +48 / −0 | Public constructor throws `E_INVALIDARG` for null args,
negative/out-of-range index or a foreign column; internal path uses
`TrustedOwnerTag` | 63787021 |
| `TableView/TableViewGroupHeader.cpp` (+ .h) | +39 / −0 | Default
count's opacity is not applied twice when disabled | 64475629 |
| `TableView/TableView.idl` | +31 / −13 | `TableViewColumnHeader`,
`TableView.ColumnHeaderStyle`,
`TableViewColumn.HeaderStyle`<br>`CanSort`/`SortByColumn`/`ToggleSortDirection`/`GridLinesVisibility`
docs; cell-peer `E_INVALIDARG`; group-header parts | 64475625, 63790855,
64143451, 63787021 |
| `TableView/TableView_Columns.cpp` | +19 / −0 | Releases removed
columns' header cells; old `UIElement` `Header` returned to the app on
change | 64475625 |
| `TableView/TableViewColumn.cpp` | +7 / −0 | `HeaderStyle` change
queues a header rebuild | 64475625 |
| `TableView/TableViewSource.h` | +6 / −0 | `IsOwnedByOther` helper for
sort sync | 63790855 |
| `CommonStyles/TabularSurfaces_themeresources.xaml` | +5 / −2 | New
`TabularSurfaceOuterBorderBrush` (Dark/Light/HC); group count opacity
documented as secondary-over-primary text (0.77 Dark) | 64475611,
64475629 |
| `TableView/TableView.vcxitems` | +2 / −1 | Swap
`TableViewHeaderCell.h` for `TableViewColumnHeader.*` | 64475625 |
| Generated / metadata (`Generated/TableView.properties.*`,
`Generated/TableViewColumn.properties.*`,
`dll-tabular/XamlMetadataProviderGenerated.h`) | +123 / −0 | Codegen for
the new properties and type | 64475625 |

**Total: 17 rows, 29 product files, +2117 / −738.** Also: 19 test files
(+3718 / −32), 3 docs (+161 / −35), 6 sample files (+2 / −9).

</details>

### New preview API (needs API review)

- **`TableViewColumnHeader`**: sealed, templated `ContentControl`,
created only by `TableView`.
  - Read-only `Column` property.
- Template parts (optional): `PART_ContentPresenter`, `PART_GridLine`,
`PART_SortIndicatorHost`, `PART_ResizeGripperHost`.
- Visual state groups: `CommonStates`, `HighContrastStates`,
`SortStates`.
- **`TableView.ColumnHeaderStyle`**: Style applied to every column
header.
- **`TableViewColumn.HeaderStyle`**: per-column Style; wins over
`TableView.ColumnHeaderStyle`.

There is no `HeaderGridLinesVisibility` property. `GridLinesVisibility`
affects body lines only; header lines are always drawn. To hide them,
override `TabularSurfaceHeaderSeparatorBrush` with `Transparent`.

### Current Behavior
- Headers are built in C++ with no template, no visual states and forced
local typography, so apps can't restyle them. Header separators don't
show.
- Header peers report `HeaderItem` without Transform, so UIA clients
can't resize columns.
- `SortByColumn` ignores `CanSort`; `CanSort=false` on a sorted column
leaves the sort in place; sorting through a source another table owns
reports success; some implicit clears raise `Sorted(None)` while the
app's sort remains.
- The cell peer constructor accepts invalid arguments; a throw in
`BeginEdit` leaves edit state half-open.
- Recycling a row rebuilt every cell, refreshed chrome up to twice and
invalidated the table's measure.
- The outer border has no dedicated brush; the default group header uses
raw weights and off-grid spacing.

### New Behavior
- Headers use the templated `TableViewColumnHeader` (see API above);
typography, padding and brushes come from Style setters.
- Header peers implement `ITransformProvider` through the existing
resize path (Min/Max clamping, scale conversion; pinned columns report
`CanResize=false`).
- `CanSort=false` blocks header clicks, `SortByColumn` (except `None`)
and `ToggleSortDirection`, and clears the column's own active sort;
sorts through a source owned by another table return false; `Sorted` is
raised only when the table's sort state changes.
- Cell peer constructor throws `E_INVALIDARG` on bad input; `BeginEdit`
rolls back on failure.
- Recycled rows reuse their cells unless cell automation peers exist;
chrome refreshes once.

> **Visible changes**
> - Outer border uses `CardStrokeColorDefault`.
> - Header lines are always shown, even when `GridLinesVisibility` is
`None` or `Horizontal`.
> - Group header restyled: 8px spacing; the count follows the header
`Foreground`.
> - Disabled header text and sort glyph dim.
> - TableView samples no longer opt out of High Contrast backplates
app-wide.

## Customer Impact

Restylable column headers, visible header separators, Fluent-aligned
outer border and group headers, UIA column resize for assistive
technology, smoother scrolling and predictable sort events.

## Regression Potential

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

Header construction (focus restore, frozen columns, UIA peer, tooltips,
resize, sort glyph) moved to a templated control. Sort event semantics
are tighter: `CanSort` is enforced on programmatic sorts and fewer
`Sorted` events fire. Outer border color and always-on header lines
change visibly. Covered by new and existing tests.

## 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

Full TableView API and interaction suites on test VMs: 877/882 pass at
224d10d5c; 3 Localization tests are [Ignore] and 2 intermittent failures
passed 3/3 on rerun. Product, test apps and TableView sample apps build
(x64 chk).

Also covered:
`VerifyDisabledGroupHeaderCountFollowsContentTemplateChanges`.

Key new tests:
- `VerifyColumnHeaderTemplateParts`, `VerifyColumnHeaderSortStates`,
`VerifyColumnHeaderTypographyHasNoLocalValues`
- `VerifyColumnHeaderStyleApplies`,
`VerifyBasedOnOnlyColumnHeaderStyleApplies`,
`VerifyIncompatibleColumnHeaderStyleIsIgnored`
- `VerifyHeaderGridLinesShowWhateverTheBodyGridLines`,
`VerifyTransparentHeaderSeparatorBrushHidesHeaderLines`,
`VerifyGridLineAndOuterBorderBrushesMatchFluentStrokeTokens`
- `VerifyDisablingHighContrastHoveredHeaderKeepsDisabledForeground`,
`VerifyDefaultGroupHeaderCountInheritsForegroundThatFollowsStates`
- `VerifyTransformResizeAccountsForAncestorScale`,
`VerifyTransformResizeSmallWidthChangeWritesOnce`,
`VerifyHeaderDisabledByStyleIsNotEnabledOrInvokable`
- `VerifySortByColumnRespectsCanSort`,
`VerifyImplicitClearKeepsSourceSort`,
`VerifyUngatingKeepsAppLitColumnWithoutEvents`,
`VerifySortOptOutDuringBeginningEditKeepsTheEditOpen`
- `VerifyRecycledRowWithoutCellPeersReusesItsCells`,
`VerifyHeaderFocusStaysInHeaderBandWhenColumnsReplaced`,
`VerifyBeginEditThatThrowsRollsBackAndAllowsALaterEdit`,
`VerifyCellPeerConstructorValidatesArguments`

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 94961bb8-d187-483c-bece-19e203752c67
2026-10-23 14:10:17 +05:30
Saharsh JaiswalandCopilot 927f474d48 TableView TiP: blame by exception type and count contained app faults
Review round 3. Attribution matched the last app HRESULT against a
control-wide counter, so a nested callback could overwrite it and an
unrelated exception with the same HRESULT could match. App code's
exceptions are now tagged where TableView calls it (CallerError, and
CallerOutOfMemory for an allocation failure, which stays a std::bad_alloc),
and the catch that decides an outcome reads the type. SelectionModel calls
TableView back across the ABI, which drops the C++ type, so the call into
it tags the exception again by its error object.

Column code is TableView's unless app code ran it: the editing hooks are
internal, so only a CellEditingTemplate's content (while the editor is
prepared) is the app's there, and only an app-derived column class owns its
display factory and sort member path. Expansion is always tracked and its
catch reads the tag; only a request that changes one group or node runs a
test.

A filter over a large source no longer drops into the small bucket: the
large flag counts the source before the filter, and the metrics and
TableView_Instance carry both counts. Initialize closes as
unloaded_before_render when the table is unloaded with rows and a body
viewport before any row was laid out. An app filter predicate, sort or group
key selector or custom comparer that throws is still contained, and now
writes one recoverable TableView_Error per operation with FaultCount.

Internal pipeline builds fail when Tabular falls back to the TiP stub. All
versions start at 1, since none has shipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-23 02:13:31 +05:30
Saharsh JaiswalandCopilot a7483e3aed Merge main into user/sahjaiswal/tableview-tip-followup
Brings in #12349 (TableView low-risk fixes) and 35 other commits from main.
No conflicts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-22 23:22:20 +05:30
Hitesh KumarandCopilot 83ebb939f7 TableView: low-risk fixes (keyboard group toggle, gripper cursor and visuals, Fluent grid lines, scroller lookup, samples) (#12349)
## Fixes

| Internal issue | Type | Change |
|---|---|---|
| https://task.ms/64512114 | Bug | Toggling a focused group header with
the keyboard no longer jumps the viewport to earlier groups |
| https://task.ms/64475631 | Bug | A missing
`TabularSurfaceGridLineBrush` is reported once per instance (debug
diagnostic) |
| https://task.ms/63787021 | Task (part) | Scroller ancestor lookup
stops at the TableView |
| https://task.ms/64475630 | Bug | Selection indicator seeded from
accent fill tiers; unused `TabularSurfaceHeaderForegroundBrush` removed
|
| https://task.ms/63790855 | Task (part) | Sort and scroller-structure
IDL docs match current behavior (comments only) |
| https://task.ms/64475633 | Bug | Samples: remove the extra card frame
around examples |
| https://task.ms/64475637 | Bug | Samples: opt out of automatic
high-contrast backplates |
| https://task.ms/64491943 | Bug | Test only: row banding follows the
element theme (app-side fix is theme-aware brushes) |
| https://task.ms/64475632 | Bug | ResizeGripper template collapsed to
one Border; overwritten row `MinHeight` setter removed |
| https://task.ms/64475628 | Bug | ResizeGripper: no accent hover line;
1px neutral line only while dragging |
| https://task.ms/64143434 | Bug | ResizeGripper shows the East-West
resize cursor; mouse captured while dragging, released on Escape,
capture loss, disable or unload |
| https://task.ms/64475611 | Bug (part) | Grid lines use the
`DividerStrokeColorDefault` values; the new outer-border key stays in
#12307 |

## PR Type

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

## Description

First of two PRs split out of #12307. This one carries only small,
low-risk fixes with **no new API** (product: +158 / −72 lines; IDL
changes are comment-only; samples +51 / −52). New API and the larger
changes stay in #12307, which is stacked on this PR.

| File | +/- | Change |
|---|---|---|
| `TableView/TableView_Keyboard.cpp` | +14 / −1 |
`OnBodyBringIntoViewRequested` ignores the focus-rescue element (row or
group header) only while a group-toggle focus restore is pending; with
no body scroller, focus reveal goes through the app's ScrollViewer |
| `TableView/TableView.cpp` (+ .h) | +11 / −4 | Missing grid-line brush
logged once; `FindScrollViewerAncestor` stops at the TableView |
| `ResizeGripper/ResizeGripper.cpp` (+ .h) | +97 / −21 | Resize cursor;
mouse capture during drag, released on Escape, capture loss, disable and
unload |
| `ResizeGripper/ResizeGripper.xaml`,
`ResizeGripper_themeresources.xaml` | +18 / −26 | Template collapsed to
one Border; hover line removed; 1px neutral line only while dragging |
| `CommonStyles/TabularSurfaces_themeresources.xaml` | +8 / −15 |
Selection indicator uses `SystemAccentColorLight2`/`Dark1`; grid lines
use `DividerStrokeColorDefault` values; unused header foreground key
removed |
| `TableView/TableView.xaml` | +0 / −2 | Overwritten row `MinHeight`
setter removed |
| `TableView/TableView.idl`, `TableViewSource.idl` | +10 / −3 | Doc
comments only |

### Current Behavior
- Toggling a focused group header near the bottom of a grouped list with
the keyboard scrolls the viewport to unrelated earlier groups.
- A missing grid-line brush is silently replaced; the scroller lookup
can walk past the TableView into app content.
- The selection indicator uses the raw accent seed color.
- The column resize gripper shows no resize cursor and draws an accent
line on hover.
- Grid lines are darker than the Fluent divider stroke.
- Samples wrap every example in an extra card and let the system draw
high-contrast backplates over the control's own colors.

### New Behavior
- The focused group header stays in view after a keyboard toggle.
- In custom templates without a body ScrollViewer, keyboard focus
reveals the focused row through the app's ScrollViewer.
- A missing grid-line brush produces one debug diagnostic per instance;
the scroller lookup is bounded by the TableView.
- The selection indicator follows Fluent accent fill tiers (small
visible color change).
- The gripper shows the East-West resize cursor and holds it during a
drag; no hover line; a 1px neutral line shows only while dragging.
- Grid lines use the fainter Fluent divider values (visible change).
- Samples show the control directly and keep its high-contrast colors.

## Customer Impact

Keyboard group toggling keeps the user's place; column resizing shows
the expected cursor and Fluent visuals; selection indicator and grid
lines match Fluent tokens; cleaner samples.

## Regression Potential

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

The keyboard guard applies only while a group-toggle focus restore is
pending. The removed theme key is a preview key that nothing reads. The
gripper now captures the mouse during a drag; capture is released on
Escape, capture loss, disable and unload. Grid lines use the Fluent
divider values, a visible but intended change.

## 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

Full TableView suites on test VMs: 639 API tests pass (3 Localization
tests are `[Ignore]`) and 150/150 interaction tests pass. Product, test
apps and TableView samples build (x64 chk). Four rounds of automated
review (code, WinUI, regression, security) until no High+ findings.

New tests: `VerifyFocusedBottomGroupToggleKeepsViewport`,
`VerifyFocusedGroupToggleAmongCollapsedHeadersKeepsViewport`,
`VerifyDroppedGroupToggleKeepsRowBringIntoView`,
`VerifyScrollerLookupStopsAtTheTableView`,
`VerifySelectionIndicatorBrushFollowsAccentFillTiers`,
`VerifyAlternatingRowBackgroundFollowsElementThemeOppositeToApp`,
`VerifyResizeGripperHoldsResizeCursor`,
`VerifyDisablingGripperMidDragCancelsResize`,
`VerifyEndDragOnUnloadedGripperRaisesDragCompleted`,
`VerifyRemovingTableMidDragCancelsResize`,
`VerifyResizeGripperSeparatorShowsOnlyWhileDragging`,
`VerifyGridLineBrushesMatchFluentStrokeTokens`; interaction tests
`GroupHeaderKeyboardToggleAtBottomKeepsViewport`,
`PointerOverGripperShowsResizeCursor`,
`PointerResizeEscapeReleasesCapture`,
`KeyboardNavigationRevealsRowInAppScrollerWithoutBodyScroller`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 94961bb8-d187-483c-bece-19e203752c67
2026-10-22 13:50:02 +05:30
Saharsh JaiswalandCopilot f6c060aeb0 Merge main into user/sahjaiswal/tableview-tip-followup
Brings in #12306 (TableView context menus) and seven other commits from main.
The only conflict was the TableView.vcxitems item lists; both sides' entries
are kept.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-20 16:09:11 +05:30
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
Saharsh JaiswalandCopilot 1f82669f7a TableView TiP: one test per key operation
Sort, filter, edit begin, edit commit, and expanding or collapsing one group
or tree node each report under their own measure and failure reason, so each
has its own pass rate. Every test's metrics bucket is the flow direction and
whether there are at least 1,000 data rows.

A source swap after Initialize, selection, a cancelled edit and group
configuration no longer run a TiP test; a failure in one still writes
TableView_Error. An exception while expanding or collapsing one group or node
is still swallowed, but now fails its test and writes TableView_Error. An
expansion request that changes nothing runs no test, since each arrow key on
a group header is requested twice.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-20 13:54:45 +05:30
Saharsh JaiswalandCopilot 6687f886d6 TableView: Initialize TiP ends at the first laid-out row
Initialize (measure 64477764) now starts once the template is applied and a
source is attached, whichever comes later, and ends at the layout pass of the
first realized row or group header, or at once for an empty view. A source set
before Initialize has its verdict is part of it, so it no longer writes its own
ReplaceSource result. A table whose source stays null runs no Initialize.

Failures carry template_setup_failed or source_attach_failed with the HRESULT;
app faults still close Ignored. A custom template without PART_RowsRepeater,
PART_HeaderRow or PART_HeaderHost closes Initialize Ignored
(app_template_incomplete) and sets TemplateIncomplete on TableView_Instance.
The metrics bucket is the flow direction and whether there are at least 1,000
data rows.

TiP data version 7 (Initialize), InstanceVersion 2, SchemaVersion 8.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-19 19:08:00 +05:30
Saharsh JaiswalandCopilot 1737209cd2 TableView: TableView_Instance replaces TableView_SessionSummary
One TableView_Instance per loaded session (written at Unloaded, the destructor
as backstop, capped at 64 per process) carries the enabled and used feature
masks, the data-row and peak-realized-row buckets, column and source kinds,
flow direction and whether the instance has shown rows. Live shaping counts as
used when it reshapes for an item change.

TableView_SessionSummary is removed and SchemaVersion goes from 6 to 7. TiP and
TableView_Error are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-18 14:16:22 +05:30
Saharsh JaiswalandCopilot 324b1243e7 TableView TiP: blame the app only when its code decides the outcome
- Recovered app faults (BeginningEdit, CellEditEnding, validation, factories, forced-close write) are reported and keep TableView's outcome; app exceptions that leave an operation make it Ignored(application_fault); a TableView failure always wins and both are reported.
- Collection reads, caller contracts and selection property observers are attributed through notes at their source instead of exception types; source swap and control sort keep the failing HRESULT.
- A parented element from a column factory is the app's fault.
- Clears are measured only when the engine has something to clear.
- Session: TableView_Loaded once per process, the summary at Unloaded or destruction and only when non-empty, pre-load source swaps measured and counted.
- TiP vocabulary moves to TableViewTipTypes.h; counters use explicit slots; data versions 6/5, schema and summary version 6.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
2026-10-15 15:39:55 +05:30
866ba81a1c TableView: merge feature/TableView into main (#12267, #12256, #12248, #12120) (#12285)
Merges the TableView feature branch `user/hik/feature/TableView` into
`main`. Each change below was reviewed and merged into the feature
branch as its own PR. This PR brings them to `main` together.

## Included PRs
- **#12267 Hierarchical and live data operations (review fixes)**
(@dipeshmsft)
  - Group/tree collapse keeps selection consistent.
  - Live-shaping subscriptions are hardened.
  - Code-review and benchmark-app fixes.
- **#12256 [2/3] TableView: cell UIA names, stale peers, grow-only Auto
width, regroup selection, HC backplates, rounded focus**
  - Cell accessible names don't repeat the column header.
  - Stale GridItem peers throw ElementNotAvailable.
  - Group StructureChanged is gated on ListenerExists.
  - Auto columns only grow within a data set.
  - A regroup keeps the moved selected item selected.
  - No high-contrast backplates on hovered/pressed rows.
- Keyboard focus rects are rounded on cells, header cells and group
headers.
- **#12248 [3/3] TableView: ListView paging and far-row focus, header
naming/focus/gripper, source ownership, safe sort, edit rollback**
- **Paging and focus:** ListView-style PageUp/PageDown (group headers
are valid targets); far-row focus on Home/End and Ctrl+Home/End.
  - **Headers:** naming, focus restore and resize-gripper fixes.
- **Source ownership:** single-owner `TableViewSource` with
release/reclaim on Unloaded/Loaded.
  - **Sorting:** fails safely over null or duplicate rows.
  - **Editing:** rollback of invalid values on forced edit teardown.
- **Sample-app findings fixed:** a layout-cycle crash, fast-paging
focus, ComboBox editor focus, Ctrl+Space selection.

- **#12120 TableView: add usage telemetry and Test in Production**
(@Saharsh979)
- Reliability (TiP) and usage telemetry on the existing Tabular
providers. No public API change.
- Two TiP tests: `TableViewInitializationTest` runs on
`OnApplyTemplate`; `TableViewOperationTest` covers source swap, sort,
filter, grouping, selection and edit begin/commit/cancel.
- Usage telemetry events: `TableView_SessionSummary` and
`TableView_Error`.
- Build: `TableView.TestInProduction.props` is imported by Tabular only,
and `MuxcTraceLogging.h` gains `KEYWORD_TABLEVIEW`.
`main` has 3 NavigationView commits that aren't on the feature branch.
They don't touch TableView, and the two sides merge with no conflicts.

## Validation
On a test VM, using the feature-branch bits before the final #12248
rebase (before #12120 landed):
- **TableView API suite:** 581/582 passed.
- **TableView interaction suite:** 122/122 passed.
- **TableViewSampleApp:** sanity pass on every page (paging, grouping,
editing, sort, navigating away and back).

- **Build fix (32f8ff44a):** #12120 added a TiP generation argument to
`SyncTableViewSourceSort`, but the `RerankCustomSortAfterReclaim` call
from #12248 still passed two arguments. Git merged the two cleanly, so
the conflict only showed up as a C2660 build error. The rerank is driven
by the control, not the user, so it now passes `0` (no TiP operation).

## Known issues
- **Unnamed visual cells (8504536eb):** a cell that shows an unnamed
visual, such as a ProgressBar, is now named after its column instead of
"blank", so `VerifyUnnamedVisualCellIsNotNamedBlank` passes (3/3 on the
test VM).
- **3 localization tests are temporarily disabled** (in #12248).
- **Follow-ups from the #12248 review:**
  - PageUp/PageDown distance with variable-height rows.
- The identity-validation cache misses changes in collections that don't
raise change notifications.
  - Interaction coverage for header hover/press visuals.
- **Theme resources:** unused `TabularSurface*` theme keys and their
`MUX_TabularSurface*` aliases were removed. They only shipped in
experimental packages, so this is worth a release note.

---------

Co-authored-by: Dipesh Kumar <dipeshkumar@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Dipesh Kumar <85861525+dipeshmsft@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hiteshkrmsft <169995515+hiteshkrmsft@users.noreply.github.com>
Co-authored-by: Saharsh Ananta Jaiswal <sahjaiswal@microsoft.com>
Copilot-Session: 1a711a5a-8e3c-4114-a58b-68a5e3784da9
Copilot-Session: 1b4b389b-9df6-4996-bd1c-b4a30a5da768
Copilot-Session: d8e58903-f3be-45f2-8124-5cdcfcca0428
Copilot-Session: b54b3a5d-2e82-43fa-9664-760e896cc4a3
Copilot-Session: 1cda586e-f933-4841-9256-463216c2c118
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
Copilot-Session: a4f445d6-e2e7-416c-a777-7bc28103da7e
2026-10-15 11:41:27 +05:30
b6d103698d [1/3] TableView: focus re-entry/restore, close fail-fast, UIA/typography fixes, new tests, sample a11y (#12220)
## Summary

Fixes a process fail-fast when a window hosting a TableView closes,
makes Tab / Shift+Tab into the table land on the remembered cell or row
(or a visible row) without scrolling, moves keyboard focus to a clicked
group header and keeps it across the collapse, clips the TableView UIA
bounds to the table, names the built-in text cell editor, rounds the
header focus visual, and lets a `FontWeight` set on the TableView reach
cell text. Makes a forced edit close cancel when the edited item was
removed. Adds 77 new tests (59 API, 18 interaction) with two TestUI
pages, and fixes the sample app's remaining accessibility test-pass
findings. Product changes are in `controls/dev/TableView`, plus
`ShapedItemsSource` (`RebuildGrouped` ordering) and
`HierarchicalSourceAdapter`; the dev spec is updated to match, and code
comments are trimmed to must-have only. Supersedes #12185 (closed).
#12256 is stacked on this PR, and #12248 on #12256.

## Fixes

Only **Fixed** rows are resolved by this PR.

| Work item | Bug | Status |
|---|---|---|
| https://task.ms/64512990 | Closing a window that hosts a TableView
fail-fasts the process (0xC0000409) | Fixed |
| https://task.ms/64396676 | Watson: fail-fast in Tabular.dll (Task
Manager) | Related: generic noexcept→terminate stack; check the cab
before resolving |
| https://task.ms/64474596 | Watson: fail-fast in Tabular.dll (C++
sample) | Related: same as above |
| https://task.ms/64512113 | Shift+Tab back into the table lands on a
different row and scrolls | Fixed |
| https://task.ms/64459543 | Focus lost on Tab from the last left-nav
element into the table | Related: retest on the current sample |
| https://task.ms/64476282 | Focus lost on Tab from 'Run all'
(Performance page) | Related: retest on the current sample |
| https://task.ms/64476063 | Built-in text column editor has no
accessible name | Fixed |
| https://task.ms/64142128 | Header keyboard focus border has square
corners | Fixed (filed against File Explorer; resolve after the package
flows) |
| https://task.ms/64475623 | Header and cell typography set as local
values | Related: cell/editor part fixed here; header weight is #12256;
FontSize pending design |
| https://task.ms/64338492 | Sample app accessibility test pass (85
sample-side bugs) | 16 fixed here; the rest triaged (see below) |

Also fixed, no tracking bug: clicking a group header doesn't focus it;
the UIA bounding rectangle includes cache rows below the viewport;
Multiply (`*`) on a click-focused tree row strands focus.

## What's fixed

**1. Window close no longer fail-fasts (64512990).**
- Cause: the deferred `Unloaded` runs after the window is destroyed.
Removing the `ThemeSettings.Changed` handler then throws `E_FAIL` from
`ThemeSettings::EnsureWindowFeatureDetached` inside the `noexcept`
`remove_Changed` in Microsoft.UI.dll, which no TableView try/catch can
contain.
- Fix: release `ThemeSettings` on the host `AppWindow.Destroying`, while
the window still exists. It is subscribed only when the window belongs
to the current thread (where `Destroying` is raised), and by token,
since `AppWindow` has no weak-reference support for `auto_revoke`.
- Other hosts (no `AppWindow`, or a window on another thread) use the
`Unloaded`/`~TableView` path: `ReleaseThemeSettings()` releases through
`::IUnknown` in try/catch before revoking, and drops the `Destroying`
subscription.

**2. Tab / Shift+Tab re-entry returns to where the user was
(64512113).**
- Cause: XAML aims entry at the first/last realized tab stop, often a
cache row below the viewport, so focus moved to a different row and the
body scrolled.
- (i) Entry from outside (`Next`/`Previous`) is redirected in
`TableViewRow::OnRowGettingFocus` via `ResolveFocusEntryTarget`: the
remembered record's cell or row, found by item (survives sort/recycle);
otherwise the first (Tab) or last (Shift+Tab) enabled row in the
viewport.
- (ii) Disabled rows are skipped when resolving the remembered row and
the viewport edge; if the remembered target can't take focus for any
reason, entry falls back to the viewport edge row.
- (iii) Guards: target must be visible, a tab stop, enabled, not under a
disabled control, inside the table; one redirect per `CorrelationId`;
cursor state committed only when `TrySetNewFocusedElement` succeeds; the
drilled container is cleared on recycle. Arrow, pointer and programmatic
moves are not redirected.

**3. Tab from the column header band lands in the viewport.**
- `TryRedirectBandEntryToViewport`, called from both the row and group
header `GettingFocus`, sends Tab from the header band to the first fully
visible enabled row or group header in the viewport (last for
Shift+Tab), never an off-screen cache container. A row at cell level
lands on its current-column cell.
- Landing on a group header leaves cell level (the column cursor is
kept), whether redirected or already visible. If no enabled element is
visible, the framework's target is kept. Entry from outside the table
aimed at a group header is not redirected.

**4. Clicking a group header focuses it.**
- `Focus(FocusState::Pointer)` on left-button release inside the band,
before the toggle; the cell cursor returns to row level. Nothing on
press, so an open editor's commit can't reshape the table mid-gesture.
- The release acts only if the container still shows the pressed group,
re-checked after `Focus()`; a press dragged off does nothing; pointer
state resets when the container is prepared or cleared.

**5. Focus restore after collapse/expand.**
- Header restores use the gesture's focus state (Keyboard for
Enter/Space/Left/Right, Pointer for a click); data rows restore only
Keyboard/Programmatic focus, except Multiply, which is treated as
Keyboard so a click-focused tree row keeps focus.
- `CanRestoreContainerFocus` skips the restore if focus left the table
or the XamlRoot host is hidden. A pointer-toggled header is not scrolled
into view.

**6. UIA bounds match the table.**
- `TableViewAutomationPeer::GetBoundingRectangleCore` intersects the
base bounds (a union of realized descendants, including cache rows) with
the table's layout box; on a transform failure or no valid intersection
it returns the base bounds, never an empty rect.

**7. Text column editor is named (64476063).**
- `TableViewTextColumn::GenerateEditingElementCore` sets the editor's
UIA Name from the existing `TryGetColumnHeaderString` helper. An
explicit `CellEditingTemplate` still returns early.

**8. Rounded header focus visual (64142128).**
- The system focus visual takes its corners from the focused `Grid`'s
`CornerRadius`; `RebuildHeaders` now sets `ControlCornerRadius` (4 px
fallback) on each header cell. The cell fill is transparent, so only the
focus visual changes.

**9. FontWeight inherits into cells (64475623, partial).**
- Removed the no-op local `FontWeight(Normal)` on the cell `TextBlock`
and editor `TextBox`. Header `SemiBold` is unchanged here.

**10. Sample app accessibility (64338492).**
- Of 85 sample-side bugs: 16 fixed, 33 already fixed, 18 UI removed, 6
stock WinUI behaviour, 9 not valid failures, 3 need a runtime check.
64479175 and 64480152 (Up/Down doesn't scroll the options rail) are not
failures.
- Actions announce their outcome (repeats re-announced);
`RailSection`/`ShapingOptions` are UIA Groups; sliders state their unit
via `ItemStatus`; text wraps at 200%; decorative glyphs hidden;
Hierarchy headings marked; "Try it" is a callout, not an `InfoBar`.
- `AGENTS.md` gains accessibility rules, including "Fix failures, not
findings".
- Each avatar gets its own `BitmapImage` (sharing one tripped a
debug-build image assert).

**11. A forced edit close no longer writes into a removed item.**
- A forced close (row removed, filtered out, or `ItemsSource` replaced)
now reports `Cancel` and doesn't write the pending value into the
removed item, matching WPF and the Community Toolkit `DataGrid`.
- When the item is still present (sort/regroup, or only under a
collapsed group or node), a valid pending value still commits.
- This holds for grouped and hierarchical tables too: removing or
filtering out the edited row while grouped reports `Cancel`; an item
only under a collapsed group or node, or kept by `ClearParentBy`,
commits.

## Behaviour changes / regression risk (medium)

- Tab/Shift+Tab entry changes for every TableView: forward Tab into a
never-focused, scrolled table now lands on the first visible row, and
Tab from the header band no longer lands on a cache row.
- The `AppWindow.Destroying` release is the fix. The `Unloaded` path
remains for other hosts (no `AppWindow`, or a window on another thread),
where `remove_Changed` after window destruction can still fail-fast
inside Microsoft.UI.dll (WinAppSDK defect, related 52527001).
- Cells no longer force `FontWeight` Normal, so an app `FontWeight` on
the TableView or an ancestor now shows in cells and the editor.
- Clicking a group header now takes focus and restores it after the
collapse.
- `CellEditEnding` from a forced close now reports `Cancel` instead of
`Commit` when the edited item was removed, filtered out, or
`ItemsSource` was replaced, and the pending value is not written.

## Testing

**Build:** the head, `06f63ef51`, builds Debug x64 with 0 errors and no
new TableView warnings (`Microsoft.UI.Xaml.Controls.Tabular.vcxproj`,
`MUXControlsTestApp.csproj`, `MUXControls.Test.csproj`). The sample app
is not in that set. `tools\Update-Snippets.ps1 -Check` passes.

**New tests (77):**
- Fix tests: 20 API (`TableViewTests`,
`TableView_Editing/Grouping/Rows/Theming_APITests`) and 9 interaction
(Keyboard 5, Pointer 2, Accessibility 2);
`MultiplyExpandsTreeRowSubtree` re-enabled.
- New suites: `TableView_Localization_APITests` (10),
`TableView_Recycling_APITests` (9), `TableView_Lifetime_APITests` (3),
`TableView_Intersections_APITests` (12),
`TableView_HierarchyLive_APITests` (5), `TableView_Axe_InteractionTests`
(4), `TableView_Touch_InteractionTests` (3),
`TableView_Virtualization_InteractionTests` (2).
- TestUI: new `TableViewTouchPage` and `TableViewVirtualizationPage`;
`TableViewPage` gains a `DisableFocusedRowButton` fixture and
non-tab-stop nav buttons.

<details><summary>Fix test names</summary>

- Keyboard: `ShiftTabReentryResumesRememberedCell`,
`ShiftTabReentryResumesRememberedRow`,
`ShiftTabReentryAfterSortResumesCellOnMovedRecord`,
`ShiftTabReentryAfterScrollLandsInViewportWithoutScrolling`,
`ShiftTabReentrySkipsCellInDisabledRow`
- Pointer: `GroupHeaderPointerClickMovesKeyboardFocus`,
`GroupHeaderPointerPressDraggedOffDoesNothing`
- Accessibility: `TableViewBoundingRectangleExcludesRowsBelowViewport`,
`TableViewBoundingRectangleStaysOnLayoutBoxWhenScrolled`
- Hierarchy: `MultiplyExpandsTreeRowSubtree` (re-enabled)
- API: `VerifyClosingHostWindowDoesNotCrash`,
`VerifyHeaderCellCornerRadiusMatchesControlCornerRadius`,
`VerifyTextColumnEditorNameComesFromHeader`,
`VerifyTableViewFontWeightReachesTextCells`,
`VerifyTableViewFontWeightReachesTextEditor`,
`VerifyGroupFocusRestoreSkippedWhenFocusLeftTable`,
`VerifyForwardTabEntryLandsOnFirstViewportRow`,
`VerifyTabFromHeaderBandLandsOnFirstViewportRow`,
`VerifyTabFromHeaderBandAtCellLevelLandsOnFirstViewportRowCell`,
`VerifyTabFromHeaderBandOntoGroupHeaderLeavesCellLevel`,
`VerifyTabFromHeaderBandOntoVisibleGroupHeaderLeavesCellLevel`,
`VerifyTabFromHeaderBandLandsInViewportInGroupedTable`
- Forced edit close (API):
`VerifyForcedCloseCancelsWhenEditedItemIsRemoved`,
`VerifyForcedCloseCancelsWhenEditedItemIsFilteredOut`,
`VerifyForcedCloseCancelsWhenItemsSourceIsReplaced`,
`VerifyForcedCloseCommitsValidValueWhenItemStillPresent`,
`VerifyForcedCloseCommitsWhenEditedItemIsOnlyCollapsed`,
`VerifyForcedCloseCancelsWhenEditedItemIsRemovedWhileGrouped`,
`VerifyForcedCloseCancelsWhenEditedItemIsFilteredOutWhileGrouped`,
`VerifyForcedCloseCommitsWhenClearParentByKeepsEditedItem`
- Virtualization: `OpenEditIsCancelledWhenItsItemIsRemoved` (replaces
`OpenEditIsClosedWhenItsRowIsRecycledByRemovingItsItem`, which expected
`Commit`)
- `ShiftTabReentrySkipsCellInDisabledRow` also asserts the row it lands
on was an enabled row in the viewport before re-entry.

</details>

**Results so far (VM, earlier heads):** window-close fix before rebase:
`VerifyClosingHostWindowDoesNotCrash` 3/3 (fail-fast without the fix),
TableView API 511/511. Recycling 8/8;
intersections/hierarchy-live/virtualization/Axe 25/25; Touch 3/3,
Virtualization 2/2. Text editor name checked manually in the sample (UIA
Name = column header).

**Forced edit close on `06f63ef51`** (VM `winui-tableview-vm`): all 9
edit-cancel tests pass. Without the fix,
`VerifyForcedCloseCommitsWhenEditedItemIsOnlyCollapsed` and the two
`*WhileGrouped` tests fail.

**Full TableView suite on `c3afccc49`** (fresh VM, one class group per
run): API 513/513 (including `VerifyClosingHostWindowDoesNotCrash` 3/3,
localization, lifetime); interaction 116/117 (Keyboard 45/45, Hierarchy
13/13 with the Multiply re-enable, Touch, Virtualization, Axe, Pointer,
Editing, Layout, ToolTips, Accessibility). The one failure,
`TableViewTests.VerifyAxeScanPasses`, flagged test-app chrome (a
duplicate app window, `RasterizationScaleTextBox`) on the first
interaction run of the fresh VM and passed 2/2 on rerun.

**Not yet run:**
- The full suite on `06f63ef51`.
- Not run since they landed: the header-band redirect and its five API
tests, the `Destroying` thread check, the viewport-edge fallback and
`ShiftTabReentrySkipsCellInDisabledRow` on the scrolling table.
- The sample with Narrator, at 200% text, or in portrait.
- No tests for the editor-commit/wrong-group race, a pointer toggle not
scrolling, a hidden host skipping the restore, or the popup/island
bounds fallback.

## Open questions

- With nothing remembered, entry lands on the first/last visible row;
ListView uses the focused index and scrolls. Needs UX sign-off.
- The UIA bounds over-report may be generic to ItemsRepeater in a
ScrollViewer; consider a core fix.
- `ThemeSettings::remove_Changed` throwing from a `noexcept` method
after window destruction is a WinAppSDK defect (related 52527001); this
PR avoids it on the TableView side.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Hitesh Kumar <hiteshkr@microsoft.com>
Copilot-Session: 4e307622-98e4-4961-aacf-3b7175bfbb34
Copilot-Session: 26fec974-5991-4f86-93a7-0345db11cdc0
Copilot-Session: 1b4b389b-9df6-4996-bd1c-b4a30a5da768
Copilot-Session: fc704b40-91b4-46d0-85a5-6266d5bfd0bb
Copilot-Session: 6a119c59-5ea3-491a-b1fc-080b9c644511
2026-10-14 12:06:31 +05:30
Hitesh KumarandCopilot 166972a99d TableView: fix the product bugs behind the ignored API tests and re-enable them (#12190)
## What this does

Several TableView tests were checked in disabled because each one
exposed a real product bug. This PR fixes those bugs and enables the
tests — both the original API tests and the interaction tests that
landed more recently.

**No public API change.** The only `.idl` edit enables the
code-generated property-changed callback for the already-public
`GroupHeaderTemplate`.

## What's fixed

**Keyboard and focus**
1. After a sort, filter or group, focus returns to the same row instead
of being lost.
2. If you press a key or click while the table is rebuilding, your
choice wins; the restore doesn't pull focus back.
3. Up/Down/Tab move correctly between the header row and the body, and
keep your column.
4. Tab moves cell by cell even when a cell hosts buttons or text boxes;
those controls still work and keep their bindings.

**Grouping**
5. A single group expanded or collapsed right after Expand all /
Collapse all is no longer dropped.
6. The same holds when Expand all / Collapse all is deferred because a
cell is being edited.

**Sorting and live data**
7. In a sorted view, a row moves to its correct position when its sort
value changes.
8. This works with live shaping on or off, without double work,
duplicates or crashes.
9. Adding, removing or replacing items while sorted stays in order and
can't crash, even if the app changes the data mid-update or from a
background thread.

**Columns and resizing**
10. Dragging a column gripper follows the mouse in both left-to-right
and right-to-left layouts.
11. Esc during a drag cancels it and restores the original width.
12. A bad Width, MinWidth or MaxWidth (NaN, infinite, negative) no
longer breaks layout.
13. Swapping the header template at runtime redraws the headers already
on screen.

**Themes**
14. Light, Dark and High Contrast pick the right colours; the gripper
uses the accent colour.
15. A brush or style set directly on the TableView now wins over shared
resource files.

**Screen reader (Narrator)**
16. A row inside a group reads as "X of Y" within that group, not the
whole list.
17. Adding, removing or reordering columns notifies Narrator.
18. Narrator can move focus to a group header.

## What changes for apps

Everything below is a bug fix unless marked ⚠️. Risk is what could still
go wrong in real apps.

| Area | What you'll notice | Risk |
|---|---|---|
| **Focus after sort / filter** | Focus stays on the same row position
instead of being lost. | Restore waits on layout and verifies itself;
could land a frame late on very large tables. |
| **Focus is no longer stolen** | A search box or a column header keeps
focus while the table re-shapes — typing in a filter box is no longer
interrupted on every keystroke. | Low. Replaces a "who started the
sort?" heuristic with a positional rule. |
| **Tab in / out of a row** | One Tab leaves the table even when a cell
template hosts a `Button`; the button is still reachable by pointer and
by Enter. | Medium — suppress/restore temporarily touches app-owned
`IsTabStop`. Restored exactly (value, binding or unset); an app-set
value in the meantime is left alone. |
| **Tab between bands** | Tabbing header ↔ body keeps your column
instead of resetting to the first. | Low. |
| **Up / Down across the band boundary** | Down from a header enters the
first row in the same column; Up from row 0 returns to that header. |
Low. |
| **Escape during a pointer resize** | Escape cancels the drag and
restores the authored width. It previously never reached the (already
correct) cancel path, because a pointer resize takes no focus. | Low —
the listener lives only for the drag. |
| **Open cell edit** | An edit is closed before the rows view resets,
instead of leaving a floating editor behind. | Low. An edit no longer
survives a sort — that is the intended behaviour. |
| **Right-to-left** | Gripper, header grid line, sort indicator and body
grid lines all sit at the logical trailing edge; pointer resize now
agrees with keyboard resize. | Medium — several visual pieces mirror
together; RTL + custom templates is a wide space. |
| **Resize separator colour** | Follows the user's accent colour per
theme and meets non-text contrast. The old colours were hard-coded,
swapped between themes, and under 3:1. | Low–Medium — visibly different
if an app expected the old line. |
| **`ResizeGripper` theme resources** | Nothing visible via TableView;
the primitive now works standalone. | Medium — a theme page that isn't
wired into the build is *silently* absent at runtime. Verified present
in the generated dictionary. |
| **Resource lookup order** | An `Application.Resources` override of a
TableView brush now actually wins, and the nearest scope wins;
element-scoped `RequestedTheme` resolves. | Medium — central and subtle.
An app relying on the old (wrong) order sees a change. Also no longer
force-creates every deferred resource. |
| **Grouped row position** | Narrator says "3 of 8" within the group
instead of counting the whole flattened list including header bands. |
Low — and faster, via a cached group index. |
| **Single-level group header** | No longer announces a meaningless
`Level` 1; reports `0`. An app-set `AutomationProperties.Level` still
wins. | Low. |
| **Template cell / header names** | `AutomationProperties.Name` on
templated content is announced instead of ignored. | Low–Medium — the
search is intentionally depth/width bounded, so a deeply buried label
may still not be found. |
| **Group header + screen reader** | Collapsing a group through UIA
keeps focus on the header, matching the Tab route. | Low. |
| **Column add / remove / replace / hide** | Raises a structure-changed
notification, so assistive tech stops working from a stale picture. |
Low–Medium — coalesced to the next tick and gated on a listener.
Header-content-only changes deliberately don't raise. |
| **Non-finite column widths** | `MinWidth`/`MaxWidth`/star factors/auto
width are normalised, so `MinWidth = +∞` no longer produces a non-finite
`ActualWidth`. The authored `Width` value isn't coerced — only its
consumption. | Low. One shared clamp policy replaces four near-copies. |
| **⚠️ Negative `MaxWidth`** | **Behaviour change.** A negative finite
`MaxWidth` (and `-∞`) now clamps to 0, then up to `MinWidth`. It
previously meant "unbounded" — the opposite of the author's intent.
`NaN` and `+∞` still mean unbounded. | **Medium — the one item worth a
release note.** A column that used to grow freely is now pinned to its
minimum. |
| **Sort key changes at runtime** | A row whose sort-key property
changes via `INotifyPropertyChanged` now moves to its correct position
by itself. | Medium — deepest change here. Only sort-key properties are
tracked, so unrelated changes cost nothing. |
| **With `IsLiveShaping` on** | No double work: live shaping (from
#12219) owns staleness and the sort-key subscriptions stand down,
handing back when it is turned off. Focus also holds its row position
across a live re-shape. | Low — ownership is handed over in both
directions, and the in-place splice stands aside while a live restore is
pending. |
| **Filter + sort together** | Correct ordering. The fast in-place
re-sort is contracted sort-only; with a filter active it could splice at
an offset derived from live predicates against stale membership. That
combination now takes the full-refresh path. | Low — deliberately trades
speed for correctness. |
| **Large / busy collections** | Sorted bulk append is no longer O(n²);
Remove/Move use the change's own index. No visible behaviour difference.
| Medium — index arithmetic. Indices are revalidated before state is
reused, degrading to a rebuild rather than undefined behaviour. |
| **Collapse All** | No longer leaves a hand-expanded group looking
expanded. Queued per-group toggles can't write an exception back after
the bulk call. | Low. |
| **`GroupHeaderTemplate` at runtime** | Swapping the template updates
headers already on screen; pooled containers are skipped so an old
template can't resurface. | Low. |

## How the fixes work

### Focus across a re-shape (sort / filter)
- A body row's focus position is **tracked as focus moves**, rather than
sampled when the re-shape happens. Every re-shape notification is raised
*after* `ItemsRepeater` has already recycled the focused row, so
sampling at that point always observed focus outside the body.
- Body focus is restored **only when focus was in the body, or was
dropped to nothing by the re-shape itself**. Focus anywhere else is left
alone: a column header keeps focus however it was activated
(Enter/Space, pointer, or a screen reader's UIA Invoke), and a control
outside the table — e.g. a search box focused by Ctrl+F — keeps focus
while it filters the table.
- The restore checks where focus is before every attempt, retries
against layout rather than a fixed number of turns, verifies the result
instead of trusting `Focus()`, and restores with the focus state the
user had.
- An open edit is closed before the rows view resets.

### Keyboard navigation
- Tab / Shift+Tab out of a row no longer moves focus itself. It
suppresses the row's cell-content tab stops for the duration of the walk
and lets XAML perform the navigation, which keeps `ItemsRepeater`'s
`KeyboardNavigationMode.Once` scope intact. The original `IsTabStop`
local value, binding or unset state is restored exactly on the next
dispatcher turn, and a value the app changed in the meantime is left
alone.
### Grouped automation peers
- Rows in a group report `PositionInSet` / `SizeOfSet` **relative to
their own group**, excluding header bands, instead of against the whole
flattened projection.
- A single-level group header no longer synthesizes `Level` 1; it
reports `0` ("not specified"), while an app-set
`AutomationProperties.Level` still wins.

### Theming and right-to-left
- The resize separator follows the user's accent colour per theme
(`SystemAccentColorDark1` in Light, `SystemAccentColorLight2` in Dark),
meeting non-text contrast in both.
- `ResizeGripper` owns its theme resources, so the primitive works
outside TableView.
- Right-to-left uses one model: `FlowDirection` mirrors the TableView
subtree, so the gripper, header grid line, sort indicator and body grid
lines all sit at the logical trailing edge.
- Element resource lookup no longer enumerates dictionaries (which
forced every deferred resource to be created), follows the framework's
local → merged → theme order, and resolves element-scoped `"Dark"` theme
dictionaries.

### Performance
- Grouped row `PositionInSet`/`SizeOfSet` use a cached group index
(O(log G) per query instead of a walk per query).
- Sorted incremental Remove/Move use the change's own index instead of
scanning the source; per-row property-change tracking only reacts to
sort-key properties; all preconditions are checked before the projection
is mutated.
- A stale-sort repair raises `ShapingChanged`, so the control treats it
as a re-shape.

### Other
- Column sizing is normalised through one shared clamp policy; a
negative `MaxWidth` now clamps to 0 rather than meaning unbounded.
- A row whose sort-key property changes is re-sorted in place; the
sort-only fast path is gated off whenever a filter is active.
- `CollapseAll` reconciles realized group headers, and a
`GroupHeaderTemplate` change re-prepares them.
- Column add/remove/replace/visibility raise `ChildrenInvalidated`,
coalesced and gated on a listener.
- Column-resize cancel and RTL gripper behaviour fixes carried over from
the earlier rounds of this PR.

## Known unrelated build issue (not fixed here)

While validating this PR we hit a pre-existing, repo-wide
incremental-build bug in `packaging/Microsoft.WinUI.csproj`, unrelated
to TableView. `package.appxfragment` is emitted by a separate target
that reuses a property computed inside `FillExtensions`; when MSBuild
skips `FillExtensions` because the manifest is up to date, the fragment
is written from an undefined property — empty — and then becomes *newer*
than the manifest, so both targets look up to date indefinitely. The
component package then carries **0 of 791** `ActivatableClass`
registrations and the test app cannot activate most WinRT types.

A clean build orders the targets correctly, so it only bites incremental
builds. **Recovery without a code change: delete
`$(OutDir)AppxManifest.xml` as well as `package.appxfragment`** —
deleting the fragment alone reproduces the empty result, because
`FillExtensions` stays skipped.

Deliberately left out of this PR: it is a packaging change with
different owners and no TableView content.

## Test infrastructure

- The frozen-column pinning test now asserts on geometry **published by
the test page**. The pin is a composition-only `UIElement.Translation`,
which no UIA `BoundingRectangle` reflects, so the previous assertion
could not observe the behaviour it claimed to test.
- Page buttons driven by **physical pointer input** are kept inside the
lab frame's 1024px clip. A clipped control still answers UIA, so it
looks present, but receives no clicks — which previously read as "the
API is a no-op".
- Axe failures now report the violating element and its ancestry instead
of only a rule ID.
- Sort/filter focus tests trigger the re-shape without moving focus, and
new tests cover a header sorted through UIA keeping focus and an outside
control keeping focus across a filter.

## Validation

Final head, built `amd64chk` and run on a Hyper-V test VM (not the dev
box), one class per `TE.exe` process:

| Suite | Classes | Result |
|---|---|---|
| TableView API tests | 36 | **465 / 465** |
| TableView interaction tests | 8 | **124 / 124** |

Interaction breakdown: Keyboard 40/40, TableViewTests 37/37, Pointer
13/13, Hierarchy 12/12, Editing 10/10, Layout 8/8, ToolTips 3/3,
Accessibility 1/1. This includes the tree-row classes that arrived with
#12219 (API `TableViewHierarchyTests` 44/44, `TableViewLiveShapingTests`
37/37).

### Rebase onto #12219

#12219 (hierarchical rows and live data operations) refactored the same
`TableView` / `ShapedItemsSource` code. Four files needed hand-merging.
Notable semantic merges:
- `ApplyIncrementalChange` drops the retained layer-1 membership
**below** the sorted path, not at the top. The stale-sort repair
re-sorts that retained state in place and the sorted splice keeps it in
lockstep; every other path there mutates `m_rows` outside `Refresh` and
still invalidates.
- `TableViewRow::OnKeyDown` is one override: the Tab tab-stop
suppression and `Multiply`-expands-subtree both run before a single
`__super` call.
- `ApplyGroupExpansionByIdentity` carries the bulk generation, its axis,
and the `subtree` flag.

An A/B against `main` on the same VM was used to confirm the hierarchy
chevron tests (`VerifyChevronClippedToLeadCell`,
`VerifyChevronClipTracksColumns`, the chevron-click interaction tests);
all pass on the final head.

### Review findings

The last commit addresses the review findings that held up when checked
against the code: re-entrant rebuild during a sorted incremental change
(stale indices used on raw `std::vector` splices), off-thread
`PropertyChanged` able to destroy the source off the UI thread,
bulk-expansion generation stamped at execution instead of request, the
re-shape focus restore pulling focus back from a row the user moved to,
own-entry detection in element resource lookup, and High Contrast
theme-dictionary variant keys. See that commit's message for detail; two
regression tests are added.

One reviewer suggestion was deliberately **not** taken: making own-entry
detection mirror `GetKeyNoRefImpl` exactly when the last merged
dictionary answers with a global value. Tabular keys are global (the
dll's theme resources), so that change sends nearly every lookup down
through the app's merged dictionaries, including
`XamlControlsResources`; on the VM it hung the test host. The
approximation is documented at `IsOwnEntry`.

Note: running the whole suite in a single `TE.exe` process destabilises
the guest, so results are from per-class runs.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 15:49:54 +05:30
Saharsh Ananta Jaiswal 4c45fb8ea9 Improve TableView resource lookup diagnostics (#12197)
Related to #12115.

Missing TableView theme resources can compile and construct
successfully, then fail during style/template realization with a generic
resource error. This adds setup guidance without changing resource
registration, exception text, HRESULTs or failure policy.

Control-specific advice stays in the Tabular DLL: informational output
to an attached debugger, at most once for the lifetime of each XAML UI
thread. Completed dictionary construction is a heuristic, not proof that
resources are merged. Equivalent resources, custom templates and late
registration remain supported.

The shared parser change is generic: use the saved XAML source for
resource-lookup tracing when `BaseUri` is absent. No control-specific
URI/type routing or new error IDs remain in shared runtime.

**Validation:** Release x64 product/native/API builds; 18 fresh native
resource/configuration cases; independent compiled
StaticResource/ThemeResource red/green with identical XBF; two-UI-thread
and late-debugger comparisons; exact API negative-path/cleanup parity.
The two compiled deferred tests, format-token guard and TableView
negative-path test also passed in OneBranch on Win10 RS5, Win11 23H2 and
Win11 25H2.

**Visual Studio — same missing HeaderBrush case, tracing off**

Before (`242bbf09` runtime): the generic missing-resource error.

![Before: generic missing HeaderBrush
error](https://github.com/user-attachments/assets/aa700ef1-66ab-41c7-9bbc-82a881d2e801)

After (`a60cb1a5` source-matched runtime): separate setup guidance with
the copyable dictionary entry and merge ordering.

![After: separate TableView setup
guidance](https://github.com/user-attachments/assets/f66447f3-0bd4-40e9-a3da-3e54d01c0869)

The original generic error remains unchanged in that same after session.

![After: unchanged generic HeaderBrush
error](https://github.com/user-attachments/assets/d0fe5326-0a42-45ab-a5c1-1b4c8a0d11d8)
2026-10-12 10:38:42 +05:30
Saharsh JaiswalandCopilot a60cb1a59b Harden TableView setup hints and cover deferred resource traces
Keep setup guidance debugger-only and scoped to the XAML UI thread.
Preserve generic exceptions and valid custom/equivalent resource arrangements.
Add compiled deferred StaticResource/ThemeResource trace regressions and clarify
the format-token compatibility guard and consumer documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7abe393-0d7b-4afe-aafd-d8b475586815
2026-10-11 05:52:03 +05:30
acfcee3641 fix: keep TableView setup guidance in the Tabular component
Use the saved XAML source only for generic resource tracing. Remove control-specific shared-runtime error routes, log conditional setup guidance once in Tabular, and make construction tracking atomic after successful initialization.

Co-authored-by: Hitesh Kumar <hik@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7abe393-0d7b-4afe-aafd-d8b475586815
2026-10-09 19:50:57 +05:30
Dipesh KumarandCopilot 9da8199d74 Trim verbose comments in hierarchy and live shaping code
Comment-only change addressing review feedback; detailed contracts moved to the design doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-09 18:49:28 +05:30
Dipesh KumarandCopilot 7e5b704bee Re-rank custom comparer rows under live shaping
A CustomSortComparer column sorted by ranks frozen at the header sort, so a live edit to the compared property never moved the row. Each live-tracked change now re-places the item with the app comparer and reshapes when any row's rank moves; coalesced bursts evict and bulk re-rank past log2(n) pending items.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-09 15:19:44 +05:30
Dipesh KumarandCopilot 5b04960b26 Harden TableView live shaping with hierarchy
- Reparent/regroup live for object-valued parent and group keys
- Copy selectors before hierarchy structure reuse (use-after-free)
- Track non-weak items via strong ref + token revoke
- Marshal off-thread PropertyChanged to owner dispatcher queue;
  drop off-owner-thread changes when no queue exists
- Revoke subscriptions only after extracting them from the map
- Snapshot group keys by group identity, not stringified key
- Reshape once when live shaping is switched on
- Raise ShapingChanged after a live restore for UIA
- Hold live reshapes while a cell is being edited; resume on edit close

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-09 15:19:31 +05:30
Dipesh Kumar 2bddd5f3ff feat(TableView): enhance hierarchical data handling with group expander support and improved index management 2026-10-09 15:19:06 +05:30
Dipesh KumarandCopilot 1371af7f50 fix(TableView): harden hierarchical rows after second review
Engine (ShapedItemsSource / HierarchicalSourceAdapter):
- Fix use-after-free when ClearParent is called re-entrantly from a callback.
- Fix array overrun when re-slicing groups after a sort.
- Defer hierarchy teardown requested during Refresh/Reslice and complete it on
  unwind (success or failure); rows fall back to inert flat rows on failure.
- Track actual publication (m_hierarchyPublished) via a PublishProjection wrapper.
- Guard teardown completion against re-entry; nested ClearParent/Refresh
  requests replay at most once after the outer attempt.
- Post refreshes deferred behind a failed pass or failed incremental source
  change to the dispatcher instead of stranding them.

Control (TableView):
- Gutter collapse collapses the parent; chevron double-press no longer starts
  an edit; deep chevrons are clipped instead of overlapping column 2.
- Keep selection by node key across rebuilds; bulk expand/collapse no longer
  ends edits on non-hierarchical sources; reapply theme to hierarchy rows.

Tests: self-check grows to 63 cases, including re-entrancy, failure-replay and
InputInjector pointer cases for the chevron and gutter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed471fcb-4192-4dbe-bd09-be236d717a67
2026-10-09 15:18:37 +05:30
Dipesh KumarandCopilot 0bc68ccd5c feat(TableView): hierarchical data rows from a flat source
Add tree-shaped rows to TableView driven by a flat items source and a
parent-key relation:

- TableViewSource.WithParent(keySelector, parentKeySelector) declares the
  relation; a second call replaces the first. ClearParent() removes it.
- Items whose parent key is not found become roots. Duplicate keys and
  cycles throw E_INVALIDARG naming the offending key. Value keys
  (numbers, strings, GUIDs, WinRT enums, Point/Size/Rect) compare by
  value; other objects compare by reference identity.
- Filtering keeps matches plus their ancestors; ancestor context rows are
  auto-expanded through an overlay that never changes the user's
  expansion state, so clearing the filter restores it.
- Works with GroupBy (roots bucketed under group headers) and sorting
  (siblings ordered by the active sort).
- Expansion state is keyed by node key and survives source changes.
  TableView.ExpandAllRows/CollapseAllRows; '*' expands a subtree.
- TableViewRow: indentation, chevron gutter pinned with frozen columns,
  Level/IsExpandable/IsExpanded state, tree keyboard (Right/Left/*),
  RTL-aware.
- UIA: ExpandCollapse pattern and Level/PositionInSet/SizeOfSet on tree
  rows; non-tree rows keep base behaviour.
- New ParentKeyIndex (TabularShaping) and HierarchicalSourceAdapter
  projector with splice updates and Reset fallback for large toggles.
- Design note: Hierarchical-Data-Rows-Parent-Key-Design.md.
- Sample: hierarchy demo page (two relations over one collection, perf
  probe) and a self-check page.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed471fcb-4192-4dbe-bd09-be236d717a67
2026-10-09 15:18:10 +05:30
Dipesh Kumar ee23471e06 Adding basic TableView tests 2026-10-06 08:37:05 -07:00
Hitesh KumarandCopilot 2def76947e TableView: close the remaining UIA accessibility gaps (#11820)
## Fixes

Fixes #
Internal Issue: https://task.ms/63757754

Also addressed here: https://task.ms/63996585 · https://task.ms/63996586
· https://task.ms/63996583 · https://task.ms/64055746 ·
https://task.ms/64055747
Resource lookup dependency, fixed upstream by #12071:
https://task.ms/64037296
Validation gate: https://task.ms/64037297

## PR Type

- [x] Bugfix

## Description

An accessibility review of `controls/dev/TableView` against the UIA
provider contracts. The peers already cover the main shape —
`IGridProvider`, `ITableProvider`, `ISelectionProvider`,
`IItemContainerProvider`, `IGridItemProvider`, `ITableItemProvider`,
`IValueProvider`, `IExpandCollapseProvider`, `IInvokeProvider` for sort,
plus sort/resize/selection announcements and RTL handling — but they
left gaps in the properties, provider identity, and keyboard paths a
Narrator user depends on.

### Current Behavior

| Gap | Symptom today |
|---|---|
| `TableViewRowAutomationPeer` overrides no `GetPositionInSetCore` /
`GetSizeOfSetCore` | Rows are virtualized, so UIA only sees the realized
window. Narrator cannot say "row 5 of 200". |
| `TableViewRow` has no stable name | The row is a `Control` with no
content of its own, so the base peer computes little. |
| `TableViewGroupHeaderAutomationPeer` has no `GetLevelCore` | Grouping
is hierarchical (`TableViewGroupInfo::Level`), but nested groups are
announced as siblings. |
| Cells have no localized control type | Every cell is announced as a
generic data item. |
| Header cell `IsTabStop` is gated on resizability alone | In the common
`CanUserSortColumns=true` / `CanUserResizeColumns=false` configuration
the header is clickable-to-sort but has no tab stop and no focus visual,
so a keyboard-only user can never sort. |
| Cell and header peers are not identity-stable | Cells were minted
fresh per query. UIA compares providers by identity, so grid addressing
and tree navigation disagreed about the same cell. |
| `ITableProvider::GetColumnHeaders` is broken in dxaml |
`CUIATableProviderWrapper::GetColumnHeaders` used `safeArray.release()`
in the OOM check. For any non-empty header set it leaked the SAFEARRAY
and returned a failure HRESULT, making column-header enumeration
effectively non-functional for **every** XAML `ITableProvider`
implementer. |

### New Behavior

| Area | New behavior |
|---|---|
| Row peer | `GetNameCore` composes visible cell text and is
**focus-independent**; an explicit `AutomationProperties.Name` still
wins. `GetPositionInSetCore` / `GetSizeOfSetCore` are **flat**, using
the same basis as `IGridProvider::RowCount` and `GetItem` — including
grouped tables, where group headers and rows are flat siblings in this
provider tree. |
| UIA property conventions | Unknown `PositionInSet`, `SizeOfSet` and
`Level` return `0`, UIA's "not specified" value for these 1-based
properties. This is a spec-conformance improvement that **deliberately
differs** from dxaml peers such as
`AutomationPeer::GetPositionInSetCoreImpl`, which return `-1`. |
| Cell peer | Adds localized control type `"cell"` while keeping
`AutomationControlType` as `DataItem`. The Tabular resource-subtree fix
is **not** in this PR — upstream #12071 landed the byte-identical change
first. With it present, a COM UIA client observes
`LocalizedControlType=cell`. |
| Header peer identity | Restores and redesigns the column-header peer
cache: keyed by logical column, peers held in `tracker_ref`, returning
the realized header-cell-owned peer when one exists and synthesizing a
TableView-owned peer only for unrealized columns. `GetColumnHeaders` and
cell `GetColumnHeaderItems` share one source of identity, and
enumeration includes scrolled-out columns. |
| Header peer behavior | The public constructor no longer throws for an
unrealized column. Synthetic `AutomationId` is restored as
`TableViewColumnHeader_<n>` unless the app supplies one; it is
creation-order dependent, not run-stable. Synthetic peers are gated by
`IsTableViewOwned()` so they do not report the whole table's bounds, a
clickable point inside the data body, or children they do not own. |
| dxaml wrapper | Fixes `CUIATableProviderWrapper::GetColumnHeaders` by
checking `safeArray.get()` instead of `safeArray.release()`, and
releases each COM interface through its own pointer. This was the lone
outlier among the 15 `IFCOOM(safeArray...)` sites in that file — 14
already used `.get()`. It is a hard **prerequisite** for the
controls-layer fix, not a drive-by. |
| Keyboard | A header cell is a tab stop when it is resizable **or**
sortable, and unmodified Enter/Space on a focused header toggles sort.
Enter fires on key down; Space arms on key down and fires on key up;
auto-repeat cannot re-sort. |
| Implementation plumbing | Adds implementation-only
`TableViewHeaderCell` and `TableViewCell` wrappers; changes the cell
wrapper from `Border` to a composed `Grid`; adds
`IVirtualizedItemProvider` to row and cell peers; adds the cell-peer
edit-name pinning state machine; adds `IsTabStop=False` to the
`TableView` default style. |
| Localization | Adds five localized resources (cell control type, list
separator, cell name format, boolean words) and culture-aware number
formatting, so an integer no longer announces as `42.00` and a decimal
no longer as `1.50`. |

### Subsequent fixes in this PR

Validation against a live sample surfaced further defects - several
introduced by earlier revisions of this PR. Each is fixed here.

| Fix | Defect |
|---|---|
| Reach every header, and every control inside a cell | Headers that
were not focusable could not be reached, and controls hosted in a cell
were unreachable. Adds `Enter` to enter cell content, `Escape` to leave
it, and `Shift+Space` for row selection. |
| Keep arrow keys inside the cell | Arrow keys escaped the focused
cell's content - from column 7 to column 9 in one case. Drill-in now
enters the first cell rather than an arbitrary one. |
| Make every row reachable, stop announcing cells twice | Grouped rows
reported "no content" / "no items in view", and every cell was announced
**twice**. The duplication was proven structurally from a UIA tree dump
rather than inferred from property spot-checks. Also fixes a `weak_ref`
crash and a duplicated occurrence. |
| Keep rows exposed after a group collapse and expand | Collapsing then
expanding a group left its rows absent from the UIA tree, because cached
peers outlived the containers they described. |
| Stop a UIA tree walk hanging the control | An identical
`FindFirst(Descendants, PropertyCondition)` took 136 ms on 8 columns and
**timed out, pinning one core**, on 17. CPU attribution placed the cost
in the control (+9.9 s per 10 s), not the client: `GetChildrenCore` ran
a focus search plus repeated `AccessibilityView` writes on **every**
query. The walk is now bounded. Also scopes cell interaction mode and
the Alt/Ctrl chords. |
| Cell classification cache correctness | The classification verdict was
keyed on content-object identity alone. Because `ItemsRepeater` rebinds
the *same* container element to a different item, a verdict could leak
between records with no state transition at all. Now keyed on content
**and** the tracked item identity, with property-changed callbacks on
`Visibility`, `IsEnabled` and `IsTabStop` held by weak-object revokers.
|
| Invalidate that cache on subtree swaps | Those properties describe
elements that already exist; none covers the subtree *gaining* one. A
`TableViewTemplateColumn` can swap content beneath a stable presenter
with no observed property changing, leaving a stale "not interactive"
verdict that would then actively hide a newly-arrived control from the
Content view. Now also observes `ContentPresenter`/`ContentControl`
`Content` and `ContentTemplate`, and `ItemsControl.ItemsSource`. |

**Root cause common to most of these:** state keyed on objects that
`ItemsRepeater` recycles. An existing container is rebound to a new
item, so any peer state keyed on container identity silently describes
the wrong record. These fixes state the peer-container-item relationship
explicitly instead of trusting object identity.

**Measured after the final fix:** 17 columns by 8 rows, the previously
fatal `FindFirst` shape at **2-4 ms**, control responsive, zero idle
CPU.

**Known residual, stated rather than papered over.** The classification
cache is derived from the visual tree, and XAML exposes no
subtree-changed event. Direct `Panel.Children` mutation, `x:Load`
realization, and mutation of an already-assigned observable items source
are therefore not observable without re-walking on every query - which
measured 1680 ms on 8 columns and a timeout at 17, i.e. it reintroduces
the hang this change exists to fix. `LayoutUpdated` would catch every
case but fires for any layout anywhere in the tree, so the cache would
be permanently stale during scrolling. Note that the timings quoted here
are from a debug build; if a retail measurement shows the uncached walk
is acceptable, the better resolution is to remove the cache entirely and
close this class of staleness outright.
### Terminal gridlines merged in (was PR #12139)

The terminal-gridline work is merged into this branch so the two land
together rather than conflicting on whichever goes second. Four files
conflicted; each was resolved as a union of both behaviours:

- `TableView.h` - keep the grouped-row refresh layout token, take the
renamed and broadened items-source collection revoker.
- `TableViewRow.cpp` `OnApplyTemplate` - keep the cells-host local that
later code uses and the cell-name reset, add the grid-line border
template part.
- `TableViewRow.cpp` - keep the cell-navigation helpers and add
`GetLastVisibleCellInternal`; both shared a trailing `return nullptr`,
so each needed its own closing.
- `TableView.cpp` - element-prepared keeps the owner-ensure call and
takes terminal suppression in place of the superseded per-row refresh;
index-changed and both teardown sites run both behaviours.

**One defect the textual merge produced, which would have shipped
silently.** `RefreshGridLines` located the last visible cell with
`try_as<Border>`. That was correct when the gridline work was written,
but this branch changes the cell wrapper to a composed `Grid`. `Grid`
does not derive from `Border`, so the cast could never match, the last
visible cell would never resolve, and terminal-gridline suppression
would never apply - while the very next loop in the same function
already used `Grid`. A textual merge cannot see this; it only surfaces
when the merged result is compared against a known-good integration of
the same two changes. Fixed to `Grid`.

The IDL delta from that work is comment-only (documenting
`PART_GridLineBorder`), so the no-API-change statement below still
holds.

**Verified after the merge** on a fresh build, deployed binary
hash-matched to the product output: 17 columns x 8 rows, all 136 cells
addressable and named, **0 duplicate cell identities**, 8/8 row peers
named with `SelectionItemPattern` and populated
`PositionInSet`/`SizeOfSet`, 17/17 headers focusable,
`Grid`/`Table`/`Selection`/`Scroll` patterns intact, `GetColumnHeaders`
returning 17, and the previously fatal `FindFirst` shape at **1-4 ms**
with the control responsive.

Not verified by me: the gridline rendering itself. That is a visual
property and this environment has no reachable interactive desktop
(screen capture and foreground activation both fail), so it needs a
human pass.
**No IDL/API surface change** — nothing in `controls/dev/Generated`
changes. There are, however, observable UIA behaviour changes: cell
`AutomationControlType` is `DataItem`; header
`PositionInSet`/`SizeOfSet` move from `-1` to `0` when unknown; and the
synthetic header `AutomationId` fallback is restored.

## Validation status

**Built clean** (product, `amd64chk`) and the accessibility surface was
verified end to end against a live sample: 150x8 grid, **1200/1200 cells
addressable and named, 0 duplicate provider identities**, 8/8 headers
focusable, `Grid`/`Table`/`Selection`/`Scroll` patterns all present,
`GetColumnHeaders` correct, and the previously fatal
`FindFirst(Descendants, PropertyCondition)` shape at **1-3 ms** (it
previously timed out while pinning a core).

Keyboard behaviour verified by `SendInput` on an interactive desktop,
with a foreground guard and a positive control so a dead injection
channel could not be misreported as a product pass: `Enter` into
interactive cell content and `Escape` back out (3/3 trials),
`Shift+Space` row selection, and sort via `Enter` and `Space` on a
sortable header. A non-sortable header correctly exposes no
`InvokePattern`.
## Customer Impact

User-facing accessibility. Narrator and other UIA clients get stable row
names, flat row position that agrees with `IGridProvider::RowCount`,
localized cells, audible group nesting, keyboard access to sortable
headers, header enumeration that includes scrolled-out columns, and
provider identity that stays consistent across table, cell and tree
navigation.

## Regression Potential

- [x] Medium risk — touches shared components or public APIs

The controls-layer changes affect TableView peer lifetime, focusability,
names, localized strings and keyboard sorting. The dxaml change is in
shared UIA wrapper code and fixes `ITableProvider::GetColumnHeaders` for
every XAML implementer, so owners of likely consumers such as
`CalendarViewAutomationPeer` should regression-check column-header
enumeration. The header tab-stop change adds stops only where a header
is actionable.

## How Has This Been Tested?

- [x] I have performed a self-review of my own code
- [ ] I have added tests to cover my changes
- [x] Existing tests pass locally — builds clean. Test coverage is going
in a **separate PR**.

**Validated by UIA against a built TableView sample** with a COM
`IUIAutomation` client: **13 PASS / 4 BLOCKED / 0 FAIL**.

| Behaviour | Result |
|---|---|
| Flat grouped row position | ✅ `PositionInSet=2`, `SizeOfSet=39`,
`RowCount=39`; index continues across a group boundary (`11,12,13 \|
boundary \| 15,16,17`) |
| `GetColumnHeaders` incl. scrolled-out / hidden columns | ✅ 16/16; 16
after scroll; 15/15 after hide |
| Header identity; cell ↔ table header provider identity | ✅ stable
runtime id; cell header item matches the table header provider |
| Row name focus-independent | ✅ identical before and after a focus move
|
| Localization | ✅ `cell`; `Platform (12)`; no raw True/False; `42` not
`42.00`; `1.5` not `1.50` |
| Cell control type | ✅ `DataItem(50029)` + localized control type
`cell` |
| `GetItem` identity and live `Column()` remap | ✅ stable id;
coordinates remap `3 → 2 → 3` across hide/show |
| Header `PositionInSet` / `SizeOfSet` | ✅ `1..16/16`; a hidden column's
header reports `0` |
| Empty row names | ✅ 0 on every page, including template-only rows |
| Group `Level` | ✅ 1 |
| `FindItemByProperty` under grouping | ✅ terminates and advances
through groups |
| Header focusability | ✅ neither=false, resize-only=true,
sortable-only=true, both=true |
| Realized `HeaderTemplate` children | ✅ Button reachable in Raw **and**
Control views |
| Header geometry stack-overflow regression | ✅ no crash reading header
`BoundingRectangle` / `GetClickablePoint` |

**Enter/Space keyboard sort — now verified.** The earlier blocker was
environmental, not a product gap: the validation machine had no
interactive desktop, so `GetForegroundWindow()` returned 0 and
`SendInput` injected 0 events. Re-run on an interactive desktop with
real `SendInput`, with two guards added after an unguarded run produced
meaningless output — abort unless the target window actually holds the
foreground, and a positive control (`Down` must move focus cell-to-cell)
so a dead injection channel cannot be misreported as a product failure.
Both guards fired during validation and voided runs that would otherwise
have been recorded as defects.

| Keyboard behaviour | Result |
|---|---|
| Sort via `Enter` on a sortable header | PASS - column reorders |
| Sort via `Space` on a sortable header | PASS - reorders again on next
activation |
| Sortable header exposes `InvokePattern` | PASS - `Invoke` +
`ScrollItem` |
| Non-sortable header omits `InvokePattern` | PASS - correct,
`ScrollItem` only |
| `Enter` into interactive cell content | PASS - focus moves into the
hosted CheckBox, 3/3 trials |
| `Escape` back to the cell | PASS - returns to the owning cell, 3/3
trials |
| `Enter` on non-interactive cells | PASS - correctly a no-op, focus
stays on the cell |
| `Shift+Space` row selection | PASS - `SelectionItemPattern.IsSelected
= true` on the row |

One correction worth recording: an initial pass reported sort as broken.
That was a bad test - it targeted a column deliberately marked
`CanSort=false`. What exposed it was that **mouse click did not sort
that column either**; a genuine keyboard-accessibility gap would show
mouse working and keyboard not. [64037297](https://task.ms/64037297) is
satisfied.

## Out of scope — follow-ups

1. ~~Confirm Enter/Space on an interactive machine before this PR leaves
draft.~~ **Done** - verified by `SendInput` on an interactive desktop;
see the keyboard table above. ([64037297](https://task.ms/64037297))
2. **Automated tests in a separate PR** for the UIA provider shape and
the keyboard behaviours validated here.
3. **Track the dxaml platform defect separately** and add an OS-side
owner of `dxaml/xcp/win/shared/UIAPatternProviderWrapper.cpp` as a
reviewer — its blast radius extends well beyond TableView.
4. **Clean up the sibling VT_UNKNOWN wrappers separately.**
`CUIASelectionProviderWrapper::GetSelection`,
`CUIATableItemProviderWrapper::GetColumnHeaderItems`,
`CUIATableItemProviderWrapper::GetRowHeaderItems` and
`CUIATableProviderWrapper::GetRowHeaders` still rely on COM identity
aliasing to stay balanced.
5. **No `ValueProperty` changed event on edit commit.**
([63787021](https://task.ms/63787021))
6. **Align the header-band tree peer with the rich header provider**
used by `ITableProvider`, so Narrator scan mode can sort.
([63996581](https://task.ms/63996581))
7. **No `ITransformProvider` on column headers.**
([63996584](https://task.ms/63996584))
8. **Possible high-contrast brush shadowing** in `TableView.xaml`.
([63996587](https://task.ms/63996587))

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7ff330f3-1185-4748-a52a-76eb70775fc1
Copilot-Session: 5984405c-a40f-44d2-95d5-98354df4ff74
2026-10-06 12:25:32 +05:30
Dipesh Kumar 04f85a0210 Resolving merge conflicts 2026-10-02 11:55:27 +05:30
Dipesh Kumar 23492d1a46 Fixing ResizeGripper and column headers edge positioning in RTL mode 2026-10-02 01:57:50 +05:30
Dipesh Kumar d9a12a259a Enabling sorting columns via keyboard 2026-10-02 01:57:50 +05:30
Dipesh Kumar 3697faa62a Removing redundant background setting of header cells 2026-10-02 01:57:49 +05:30
Hitesh KumarandCopilot 64cb5cfd83 TableView: honor CanResize and keep a Star resize inside the viewport (#12083)
## Problem

A column resize was clamped only to the dragged column's own
`Min`/`MaxWidth`, ignoring what the other columns could absorb. Two
consequences:

1. **`CanResize=false` had no effect on redistribution.** It stopped a
gripper appearing on that column, but a drag on the *neighbouring*
column still squeezed it down to its `MinWidth`. A column the app marked
non-resizable was resized indirectly.
2. **The dragged column grew without bound.** Star columns divide the
viewport, so the surplus became horizontal overflow with the trailing
column clipped to a sliver behind a scrollbar.

Reported against the Windows File Explorer property sheets, which use
two `*` columns with `MinWidth="50"` and the second column
non-resizable.

## Change

New `TableView::ResizeBoundsForColumn` bounds the gesture by what the
other columns can actually give:

- a resizable **Star** column may yield space, down to the floor the
Star pass clamps it to — only Star columns are re-divided when the
dragged column's width changes, so only they can donate;
- everything else — `Pixel`, `Auto`, or a column the user may not resize
— holds its current width;
- when no neighbour can participate the gesture is **pinned**, not
merely capped. Bounding only growth would let a drag hand width to a
column that is then not allowed to give it back, so the column could
shrink but never return;
- with no Star column anywhere, or no resolved viewport, the gesture
stays unbounded and the extent scrolls exactly as before.

`TableViewColumn` gains an internal **authored width**: the width the
app assigned, which a user resize does not overwrite. This is needed
because a resize rewrites `Width` as pixels and the keyboard path runs a
*complete* drag per keypress — without it, from the second arrow press
the column no longer reports that it was authored `Star` and the bound
disappears.

Both input paths are covered: the keyboard step drives the same gripper
drag.

5 files, +259 / −60.


## Behaviour model

The rules below decide every case. `Predict-Model.ps1` implements them
and reproduces all 25
configurations exactly, so a drift between stated behaviour and actual
behaviour fails.

**Donor** - a column that can give width away: currently `Star`,
user-resizable, **and positioned
after the dragged column**. Only Star qualifies, because the layout pass
only re-divides Star
columns; a `Pixel` or `Auto` column never shrinks in response to a
neighbour's drag. Star-ness is
read from the column's *current* `Width`, because that is what the
layout pass re-divides - a
column an earlier drag rewrote as pixels donates nothing and reserves
its actual width instead.

**Direction.** A resize takes space only from the columns *after* the
dragged divider, matching
WPF DataGrid. Star columns before it are frozen at the width they
already render, which keeps
them out of both the bound and the redistribution pass without changing
what the user sees.
Measured on `S,S,S` at 159/160/159, dragging divider 1 by +60: the first
column previously moved
to 127 and now stays at 159 (WPF: 159).

**Divides the viewport** - any visible column is authored `Star`,
including the dragged one.

Evaluated in order:

| Condition | Outcome |
|---|---|
| No column authored `Star` | **Unbounded** - the column grows, the
extent exceeds the viewport, a scrollbar appears |
| `Star` present, no donor among the others | **Pinned** - the divider
does not move |
| `Star` present with at least one donor | **Capped** at `viewport -
reserved` |

What each other column reserves:

| Column | Reserves |
|---|---|
| Donor (free Star) | its `MinWidth` |
| `Pixel` | its pixel width |
| `Auto` | its content width |
| Locked `Star` | its authored share: `(viewport - authored non-Star
widths) x factor / total authored factors` |

Worked examples: `S,S!` has no donor, so the divider pins at 239/239.
`S,S!,S` reserves the
locked column's authored share of 159 plus the donor's 50, capping the
drag at 269 for 269/159/50.
`P,P` has no Star at all, so it grows and scrolls.

**Where WPF differs.** WPF's donor rule is simply "resizable", with no
Star requirement, because
its resize *assigns* the neighbour a new width instead of relying on a
redistribution pass. So WPF
shrinks a `Pixel` neighbour and gives 428/50 for `S,P` where this pins
at 239/239. Promising
space that a `Pixel` neighbour never actually surrenders is what
produced the original overflow.

## Testing

Two paired samples were written for this — a WinUI `TableView` repro and
a WPF `DataGrid` reference — both driving the **real** resize gripper
(WPF via `PART_RightHeaderGripper` thumb events, WinUI via the header
keyboard step), so the actual redistribution policy runs rather than a
direct `Width` assignment.

**25 configurations × 4 drags × 3 runtimes = 300 measurements.**

Config syntax: one spec per column, `P` = Pixel 239, `S` = Star `1*`,
`A` = Auto, trailing `!` = `CanResize=false`. Viewport 478px, `MinWidth`
50 on every column, 4 rows.

Exhaustive two-column matrix (3 modes × 3 modes × locked/unlocked = 18):

```
P,P  P,P!  P,S  P,S!  P,A  P,A!
S,P  S,P!  S,S  S,S!  S,A  S,A!
A,P  A,P!  A,S  A,S!  A,A  A,A!
```

Three-column set (7): `S,S,S` `S,S!,S` `S,S,S!` `P,S!,A` `A,S!,S`
`S,P!,S` `S,A!,S`

Drags per configuration: `+60`, `+220`, `+600`, `-220`, each from a
fresh reset.

### Invariants

Pixel-for-pixel equality with WPF is the wrong bar — WinUI's `Auto`
columns measure wider than WPF's (115 vs 92 here; different cell
padding) and the 8px keyboard step quantises a requested `+60` to 64.
These two invariants are immune to both:

1. A locked column must not be resized by a neighbour's drag.
2. A layout containing Star columns must not be driven into horizontal
overflow.
3. Nor may it overflow **at rest**, before any input — a rounding
overshoot shows up with no drag at all.

Checked with zero pixel tolerance.

| Runtime | locked rows | locked MOVED | star rows | OVERFLOWED |
AT-REST OVERFLOW |
|---|---|---|---|---|---|
| WPF DataGrid (reference) | 60 | **0** | 68 | **0** | **0** |
| TableView before | 60 | **28** | 68 | **38** | **8** |
| TableView after | 60 | **0** | 68 | **0** | **0** |

**All three invariants now hold on every row, matching the WPF DataGrid
reference exactly.**

### Headline case — two `*` columns, second non-resizable

| Drag | Before | After | WPF |
|---|---|---|---|
| +220 | 527 / 50, 99px unreachable | **239 / 239** | 239 / 239 |
| +600 | 1127 / 50, 699px unreachable | **239 / 239** | 239 / 239 |

### No regression to horizontal scrolling

A Pixel-column table has no Star column, so it stays unbounded and still
scrolls: 208 → 688 with 478px scrollable.

## Three-column redistribution

A locked Star column was still re-divided by `ResolveColumnWidths` after
the gesture bound had
done its job. A locked Star column is now resolved from the **authored**
layout - its share of the
viewport under the authored star factors - and held fixed while the free
Star columns divide what
is left. An authored `Auto` column contributes its content width rather
than whatever a resize
gave it, so dragging an `Auto` column cannot change what the authored
layout leaves either.

This lands on WPF's numbers exactly:

| Config | Drag | Start | WPF | This PR |
|---|---|---|---|---|
| `S,S!,S` | +220 | 159 / 159 / 159 | 269 / 159 / 50 | **269 / 159 /
50** |
| `S,S,S!` | +220 | 159 / 159 / 159 | 269 / 50 / 159 | **269 / 50 /
159** |

### Layout rounding

Star widths are snapped on the **running total** rather than per column.
Rounding each column
independently lets the snapped widths sum to more than the space being
divided — with three `1*`
columns in a 478px viewport that produced 160/159/160 = 479, a permanent
one-pixel horizontal
scrollbar with no input at all. Rounding the cumulative edge and taking
differences keeps the sum
exact, which is what invariant 3 above checks.

## File Explorer's shipping configurations

Read from `FileExplorerManaged` source and swept on both runtimes. These
use non-uniform star
factors, the default `MinWidth` of 20, and up to four columns - none of
which the generic matrix
covers.

| Surface | Config | MinWidth |
|---|---|---|
| Recycle Bin (`RecycleBinPropertiesView.xaml:125-149`) | `S,S!` | 50 |
| Digital Signatures - Embedded
(`DigitalSignaturePropertyPage.xaml:52-77`) | `S2,S,S2` | 20 |
| Digital Signatures - Catalog (`:97-128`) | `S2,S2,S,S2` | 20 |
| Previous Versions (`PreviousVersionPropertyPage.xaml:105-127`) |
`S2,S3,S2` | 20 |

| Surface | Drag | Start | WPF | This PR |
|---|---|---|---|---|
| Recycle Bin | +600 | 239/239 | 239/239 | **239/239** |
| DigSig Embedded | +600 | 191/96/191 | 438/20/20 | **438/20/20** |
| DigSig Embedded | -220 | 191/96/191 | 20/153/305 | **20/153/305** |
| DigSig Catalog | +600 | 137/137/68/137 | 418/20/20/20 |
**418/20/20/20** |
| DigSig Catalog | -220 | 137/137/68/137 | 20/183/92/183 |
**20/183/92/183** |
| Previous Versions | +600 | 137/205/137 | 438/20/20 | **438/20/20** |
| Previous Versions | -220 | 137/205/137 | 20/275/183 | **20/275/183** |

Identical to WPF at saturation and on shrink; no overflow anywhere. The
`+220` rows differ by
~4px only because the 8px keyboard step delivers 224 rather than 220.

Only Recycle Bin has a locked column, so it is the only surface the
`CanResize` half of this
change affects. The other three are all free Star and simply gain the
viewport cap.

**Consumer note, not a control defect:** with the default `MinWidth` of
20, a full drag
legitimately squeezes the remaining columns to 20px each. Both
invariants hold, but 20px columns
are unreadable - those three surfaces should consider setting
`MinWidth`.


## Could the app just configure around this?

No. Raising `MinWidth` limits how narrow the trailing column gets; it
does nothing about how
wide the dragged column grows. On the unfixed control the drag had no
upper bound at all, so a
larger minimum makes the overflow *worse* - the trailing column refuses
to shrink as far, and
more of the total spills past the viewport.

Measured, two star columns, 478px viewport, full drag:

| Control | `MinWidth` 50 | `MinWidth` 120 |
|---|---|---|
| Before | 839/50, **411px overflow** | 839/120, **481px overflow** |
| After | 428/50, fits | 358/120, fits |

So the overflow is a control defect that no consumer setting avoids, and
it affects any app using
Star columns. Honouring `CanResize` is likewise a contract the app
cannot work around.

## Behaviour change worth noting

With two `*` columns where the second is non-resizable, the divider now
does not move at all, because there is no eligible column to take space
from. This matches WPF DataGrid. Apps that want a draggable divider in
that configuration should make the second column resizable or raise its
`MinWidth`.

Apps relying on the previous behaviour will see a change — but they were
getting a column resized that they had asked not to be.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-01 00:22:54 +05:30
Hitesh KumarandCopilot 50e8f50131 TableView: trim column header text instead of clipping it (#12084)
## Problem

A column header wider than its column is hard-clipped mid-glyph, while
the cells beneath it ellipsize correctly — `Department` renders as
`Departmen`, directly above cells reading `harper.zhang.00…`. Most
visible at high text scale, where a clipped header leaves the column
unidentifiable.

Two causes:

1. A string header is assigned straight to a `ContentPresenter`, which
renders it through an implicit `TextBlock` carrying no `TextTrimming`.
Cells set `CharacterEllipsis` explicitly; headers never did.
2. The sort chevron is drawn by a sibling overlaying the same `Grid`
cell, and the header presenter reserves no width for it, so header text
runs underneath the glyph.

## Change

- **Trim header text.** For a string header with no
`HeaderTemplate`/`HeaderTemplateSelector`, supply the `TextBlock`
explicitly with `CharacterEllipsis`. Non-string headers keep the
`ContentPresenter`'s own content model, so a `UIElement` header or an
implicit `DataTemplate` is untouched.
- **Give the chevron its own column.** The header cell gets two columns
— star for content, `Auto` for the indicator — so the chevron reserves
its realized width instead of overlaying the text. The grid line and
resize gripper span both.
- **Reserve that width only while sorted.** `SortIndicator` has a fixed
themed `Width` and merely fades via `Opacity`, so an always-visible host
would cost 16px on every sortable column. The host is collapsed until
the column is sorted, and `RefreshSortIndicators` keeps it in step with
a programmatic sort.

## Notes

- **RTL:** column order is set explicitly, matching how the grid line
and gripper already pick their trailing edge.
- **Auto sizing:** only a sorted `Auto` column measures wider, by the
indicator width.
- **Accessibility:** the header cell's automation name is unchanged; the
new `TextBlock` is `Raw` so the text is not announced twice.
- **Tooltips are unchanged.** No platform grid (WPF DataGrid, WinUI,
HTML) shows a tooltip on truncation by default, and the grids that do
expose it as an opt-in. `HeaderToolTip` and `CellToolTipBinding` remain
the opt-ins here. A trimming-triggered tooltip covering headers *and*
cells uniformly is worth doing separately — it is new API surface with a
per-cell cost in a virtualized grid.

## Validation

Built with `Build.cmd product`, then `pack.component.cmd` +
`Samples/TableViewSampleApp`. Note `Build.cmd mux` does **not** compile
`TableView.cpp` — it builds only `dxaml\...\Microsoft.ui.xaml.vcxproj`.

Run against `TableViewSampleApp`:

**A header too wide for its column now ellipsizes instead of
hard-clipping mid-glyph**, degrading the same way as the cells beneath
it — `NoSortTip` and `NoSortBare` on the Cell tooltips page:


![trimmed](https://github.com/user-attachments/assets/c27392a6-2840-4c0d-8310-17735fa63a95)

**Unsorted sortable columns keep their full text width**, matching a
baseline build:


![headers](https://github.com/user-attachments/assets/b45d4d0e-0dea-426b-80fa-9862b63347bd)

**Sorting a column places the chevron beside the header text**, with no
overlap:


![sorted](https://github.com/user-attachments/assets/87ec97f6-9b6b-4abf-b109-f701847a2639)

Templated and non-string headers are unaffected.

## Not yet covered

- No API tests. `controls/dev/TableView/` has no test project today.
Worth pinning: ellipsis on an over-wide header, sorted vs unsorted
`Auto` width, RTL reserving the mirrored edge, and a `HeaderTemplate`
column keeping its own content.
- RTL, density and high contrast are reasoned about but not yet verified
on screen.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-30 18:17:51 +05:30
Dipesh Kumar 8c2662b3d1 Fixing review agent findings 2026-09-07 17:42:55 +05:30
Dipesh KumarandCopilot 1fa9b92349 feat(tableview): converge sort front-ends, add path sort
TableView and TableViewSource each declared their own sort axis with no
awareness of the other, so a fluent Sort and a header click stacked two
axes and precedence silently went to whichever was declared first while
the chevron advertised the other. Sorting is now last-writer-wins: a
control-initiated sort clears every foreign axis, and an app-declared
sort clears the control's and reconciles the chevrons.

Reconciliation could only clear chevrons, because TableViewSource.Sort
takes an opaque key selector and nothing names the property it sorts on.
Adds Sort(String sortMemberPath, SortDirection): the path is evaluated
by the same resolver a column uses for SortMemberPath, so the control
can attribute the axis to a column and light its indicator. The key
selector overload stays as the escape hatch for computed, multi-field
and custom-comparer keys, and stays deliberately chevron-less - guessing
which column an anonymous key meant is worse than showing nothing.

The axis is tokened by path so re-sorting one column replaces its axis
rather than stacking a second; distinct paths still compose, which is
how a multi-key sort is declared.

SortMemberPathResolver moves out of TableView_Sort.cpp into its own
header so the column front-end and the fluent verb cannot disagree about
what a path means.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-07 17:41:43 +05:30
Dipesh KumarandCopilot b2aedb5fe1 Port TableView group-header focus + keyboard-nav fixes from internal lift
Restores keyboard focus to a group header after expand/collapse reshape
recycles its container, and teaches arrow navigation/selection to treat
TableViewGroupHeader as a valid focus anchor in the flat grouped
projection (headers are recognized but never selected).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 22e963e3-084f-44c9-be76-8bf90278b146
2026-09-07 17:41:43 +05:30
Dipesh KumarandCopilot 714f0253dd Port TableView grouping fixes + GroupedSourceAdapter rename from internal lift
- A1: keyboard expand/collapse for group headers (Enter/Space toggle, Left/Right
  collapse/expand, RTL-aware) with the header now a tab stop + focus visuals.
- C1: TableViewGroupInfo.Key exposes the actual group key instead of the internal
  ShapedGroup wrapper.
- O1: neutralized internal review comments in the grouped source adapter.
- Programmatic sort now refreshes the column sort chevron: RefreshSortIndicators
  walks the header cell for the SortIndicator (typed child-walk) instead of
  FindName, which failed for the code-created, non-namescoped visual.
- Header-click sort: sortable header cells get a transparent Background so the
  whole cell is a reliable tap target, not just the header text glyphs.
- Rename FlatGroupedSourceAdapter -> GroupedSourceAdapter across the class, files
  (GroupedSourceAdapter.{h,cpp}), the FlatGroupedSourceAdapterPtr alias, includes,
  and the shared vcxitems.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 22e963e3-084f-44c9-be76-8bf90278b146
2026-09-07 17:41:43 +05:30
Dipesh KumarandCopilot 7c147b5c6f Port TableView data-shaping grouping from internal lift
Ports the grouping feature (layer 3 of the Tabular shaping stack) from the
internal microsoft-ui-xaml-lift branch onto public main, on top of the already
-ported sort + filter core (#11644).

Adds the hierarchical/grouped source stack and the row-metadata projection that
the TableView consumes to render group headers:
  * HierarchicalItemsSource: GroupContract + GroupExpansionModel
  * GroupedSourceAdapter: FlatGroupedSourceAdapter + GroupedEntry
  * ShapedItemsSource: ShapedGroup + grouped RowIdentity handling
  * TableView: RowMetadataProvider, TableView_Grouping, flat row adapter,
    group-header automation peer, keyboard/layout integration; removes the
    superseded TableViewProjectionView.

Control/product code only; the lift sample-app rewrite is intentionally not
ported (matching the sort+filter port, whose public sample is a different app).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 22e963e3-084f-44c9-be76-8bf90278b146
2026-09-07 17:41:43 +05:30
Hitesh KumarandCopilot 8d73126ac5 Tabular: add opt-in column-header tooltips to TableView (#11764)
* Tabular: add opt-in column-header tooltips to TableView

Adds `TableViewColumn.HeaderToolTip`, the header-side counterpart to the
per-cell `CellToolTipBinding` that shipped in #11669. Every mature grid pairs
the two; a header that ellipsizes or abbreviates has nowhere to put the full
label today.

A dependency property typed `Object`, not a `Binding`: a header is not bound
against a row, so there is nothing to defer. The value is the tooltip's
content, not a `ToolTip`, and the control owns the `ToolTip` and its placement
(`PlacementMode.Mouse`) - the same contract as the cell path, including
leaving a tooltip the app set alone.

The tooltip is attached to the header cell root, so it covers the header's
content, its padding and its sort affordance.

Header cells are rebuilt wholesale rather than recycled, so there is no
binding, no attached value and no refresh path: the content is read from the
column when the header is built, and re-applied in place when the property
changes. Re-applying rather than rebuilding keeps header focus and the sort
affordance intact, and deliberately avoids `OnColumnHeaderChanged`, which
recomputes the column's Auto width. Owned tooltips are closed before
`RebuildHeaders` discards the band - clearing an open tooltip's element is the
reentrant `CPopup::RemoveChild` shape.

Accessibility: string content is reported through
`TableViewColumnHeaderAutomationPeer::GetHelpTextCore`, composed with the
column's sort state so neither is dropped. The header peer is virtual - owned
by the TableView, not by the header element - so `AutomationProperties.HelpText`
on the element would never reach a client; the peer publishes the text itself.
As with cells, non-string content is mouse-only.

Also corrects the functional spec, which claimed header tooltips could never
open because the header band is `IsHitTestVisible="False"`. That attribute is
on the row's selection indicator; the header band is hit-testable, which is
what makes click-to-sort and resize work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tabular: keep header help text from throwing, and cover it in the sample

Found by running the sample against a clean build.

TableViewColumnHeaderAutomationPeer::GetHelpTextCore threw
RESOURCE_NOT_FOUND (0x80073B17) out into UIA for every sortable column,
so a client asking a header for its help text got an exception instead
of the sort state. That predates this branch - the sort-state lookups
are unchanged - but this change adds a second lookup on the same
unprotected path, so the lookups now degrade to empty instead of
throwing.

Sample: adds the header-tooltip cases and two probes that read what a
client sees - the ToolTip attached to each header cell, and each header
peer's help text via ITableProvider.GetColumnHeaders. Adds two
unsortable columns so the tooltip-only and empty paths are observable
without the sort state, and buttons that mutate a HeaderToolTip in
place and force a header rebuild.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tabular: address header-tooltip review findings

From two orchestrated reviews of the branch. Each finding was checked
against the running sample before and after.

- The header cell root had no Background, so a Panel with no child at
  that point took no pointer input: hit-testing the trailing edge of
  every header missed the cell entirely, and the tooltip could only
  open over the header glyphs. The row cell wrapper already sets a
  transparent fill for the same reason. Verified by probing each header
  near its trailing edge - all MISS before, all HIT after. This also
  restores the click-to-sort target over the whole cell.

- ApplyHeaderToolTip now contains exceptions, matching the cell path.
  Both call sites run where an escaping exception is a fail-fast: the
  DP-changed callback, and the header rebuild. Reachable today by
  assigning one UIElement to two columns' HeaderToolTip - the second
  Content assignment throws because the element is already parented.

- ReleaseHeaderToolTips snapshots the children before closing anything.
  Closing a tooltip raises Closed into app code, which can mutate
  Columns and re-enter RebuildHeaders, clearing the collection the loop
  was indexing with a cached count.

- OnApplyTemplate releases the old band's tooltips. It already unhooks
  the header host there, and replacing m_headerHost otherwise abandons
  live popups still hosting content parented into the old cells.

- The header peer reads HeaderToolTip from the column instead of the
  realized header, so help text no longer depends on whether the band
  is currently realized, and no longer costs two band scans and a peer
  allocation per UIA query. TryGetOwnedToolTipText goes with it.

- Non-string header content gets a debug diagnostic: it works on hover
  but is not reported to assistive technology, and nothing else told
  the author.

- Spec: state that a ToolTip is not a valid value, that a UIElement
  needs one instance per column, and that rich content should be paired
  with Header text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tabular: tighten comments on the header-tooltip change

Trim to the load-bearing why, and make ReleaseHeaderToolTips private -
it has no callers outside TableView.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tabular: cache the header cell fill per rebuild

Drops a static helper that duplicated the row cell wrapper's file-local
TransparentBrush. RebuildHeaders already caches per-pass values, so the
fill joins them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tabular: share the non-string tooltip diagnostic, and trim comments

The "content is not a string" diagnostic moves into SetOwnedToolTip, so the
cell path emits it too - both paths have always had the limitation, only the
header path reported it. Comments cut back to the load-bearing why.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-04 14:28:22 +05:30
Hitesh KumarandCopilot fa7cda27ef Tabular: add opt-in per-cell tooltips to TableView (#11669)
* Tabular: add opt-in per-cell tooltips to TableView

TableView text cells render with CharacterEllipsis and no wrapping, so a value wider than
its column is unreadable and there is no way for an app to surface the full text.

Adds a CellToolTipRequested event: the handler receives the row's Item and the cell's Column
and sets Content, which the control shows as that cell's tooltip. It is opt-in - with no
handler attached the control does no per-cell work and allocates nothing - and it is resolved
per cell as rows realize and recycle, so a virtualized table only pays for what is on screen.

The control owns the ToolTip object. Content is the tooltip's content, not a ToolTip to
attach: ToolTipService rebinds a ToolTip's owner and container on every registration, so a
single instance handed back for several cells would leave them contending for one owner slot.
A UIElement returned as Content is parented by that cell's ToolTip, so a handler returns a
fresh element per raise.

Ownership is recorded rather than inferred. A private attached record holds the ToolTip the
control attached and the exact HelpText it published, tested against the raw value on the
element, so a tooltip the app set - including a bare string - is detected and left alone.

Accessibility: the tooltip text is published as the cell's AutomationProperties.HelpText, and
the cell automation peer suppresses it when it merely repeats the cell's own text so the value
is not announced twice. The suppression is resolved in the peer rather than when the tooltip
is attached: at attach time the cell content has just been generated and its binding has not
produced a value, so the two never compared equal. Resolving it in the peer also keeps
automation peer creation off the realization path. When Content is not a string the handler
sets AutomationHelpText, since the cell wrapper is internal and an app cannot reach it.

Re-entrancy: the event reaches app code from the cell realization path, so the pass snapshots
its targets rather than walking the live children collection and holds a strong reference
across the raise. The editing cell is re-read after each raise, because a handler can start an
edit on the cell being resolved; a refresh requested from inside a pass, such as an edit
closing under a handler, is recorded and replayed. Rebuild and refresh share one bounded drain,
so app code is never raised more than a fixed number of times per row. A handler that throws
loses only its own cell's tooltip, and InvalidateCellToolTips is coalesced onto the dispatcher
and bounded, so calling it from a handler cannot queue a callback every tick.

Recycling: a recycled row never shows or announces a previous item's tooltip, including after
the last handler is removed. Control-created tooltips are neutralized in place rather than
detached, so scrolling reuses the object instead of allocating a Control per cell per scroll,
matching TabViewItem.

Column-header and group-header tooltips are deferred. The header band is IsHitTestVisible=
"False" and is scrolled programmatically from the body, so a tooltip there could never open;
both surfaces land with the work that makes them interactive.

Adds a sample page covering the contract: string content, non-string content with
AutomationHelpText, a column that returns nothing, a column whose own template owns its
tooltip, and toggles for late attach, re-entrant invalidation and a throwing handler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tabular: make cell tooltips declarative via TableViewColumn.CellToolTipBinding

Replaces CellToolTipRequested with a binding on the column. The binding is set
once on the cell wrapper, so the row's DataContext drives it: a recycled row
re-resolves through ordinary inheritance and a source PropertyChanged updates a
live tooltip in place.

Because the control no longer calls app code while realizing a cell, the
re-entrancy surface disappears with it - the per-row drain loops, the pending
flags, the dispatcher pass counter and InvalidateCellToolTips() are all gone.
Computed content is authored with an IValueConverter.

HelpText suppression in TableViewCellAutomationPeer::GetHelpTextCore is now
gated on the ownership record, so it never drops text the app set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b

* Tabular: fix two review findings

- Restore the UTF-8 BOM on TableView.cpp, stripped while excising the
  tooltip event machinery.
- Restore the two [TestProperty("Ignore", "True")] attributes on
  InkToolbarTests.cs. They were dropped accidentally, reverting
  "Temporarily skip failing InkToolbar cleanup tests (#11733)" and
  re-enabling two tests the team had deliberately skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b

* Tabular: trim the cell-tooltip change to what the feature needs

- Restore the OnDataContextChanged re-entry guard and the original
  RebuildCells comment. They were removed so the tooltip drain could
  record pending rebuilds; that machinery is gone, and the guard in
  RebuildCells was always the real one.
- Revert the GetCellValueTextFromWrapper extraction. It was pulled into
  TableViewAutomationHelpers.h to share with the tooltip pass; the peer
  is the only caller again, so the body moves back inline.
- Keep the AbandonCellEdit local named cellWrapper; only the hoist out of
  the if is needed.
- Tighten comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b

* Tabular: address cell-tooltip review findings

Findings from an orchestrated multi-lens review of the tooltip change. Only
the ones that verified against current code are here; a high-severity
"recycled cells lose their tooltip" finding was disproved by a liveness probe
(all realized rows keep live tooltips and HelpText; the neutralized ones are
pooled rows, which is correct) and is not fixed.

- Drop the unreachable null-binding branch in ApplyCellToolTipBinding. Its one
  caller is guarded by `if (auto const toolTipBinding = column.CellToolTipBinding())`,
  so the binding is never null.
- EnsureCellToolTipRecordProperty returns void; its only caller discarded the
  DependencyProperty and called it purely to force lazy registration.
- Diagnose a ToolTip handed back as tooltip content instead of swallowing it.
  ToolTipService.ToolTip does accept a ToolTip, so this is an easy authoring
  mistake with no other feedback.
- Spec: state plainly that non-string content is mouse-only with no accessible
  representation, and that TabViewItem/NavigationViewItem refuse such content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b
2026-09-02 11:33:26 +05:30
e8e13dd87f ResizeGripper: framework-internal, dual-axis resize gripper primitive (#11640)
* ResizeGripper: framework-internal, dual-axis resize gripper primitive

Adds ResizeGripper, a framework-internal primitive that owns the mechanics of
dragging a divider to a new size, and consumes it from TableView for column
resize.

The primitive is a delta source: it owns no value, no bounds and no layout.
TableViewColumn already owns Width/MinWidth/MaxWidth/ActualWidth, so the gripper
reports how far the user has dragged and the host decides what that means and
clamps it. BeginDrag -> DragDelta* -> EndDrag(canceled); DragDelta carries both
the change since the last raise and the total from the gesture start, so a host
applies the total against its captured anchor rather than accumulating error.
DragCompleted.Canceled separates a torn-down gesture from a release, and the
host restores the authored GridLength on cancel so an Auto or star column is
never silently rewritten as fixed pixels.

Pointer input arrives as manipulation events, so capture, touch and pen contacts
and multi-pointer arbitration come from the gesture recognizer rather than from
each host. ManipulationContainer lets the host name the frame drags are measured
in: the gripper travels with the edge it drags, so the default frame feeds its
own motion back into the gesture and stalls it.

DragOrientation names the axis the drag is measured along, which for a divider
is perpendicular to how it looks. It is deliberately not called Orientation --
the primitive owns no shape, and that name carries the opposite meaning in other
sizer prior art. Deltas are logical, so positive always grows in reading order;
only the horizontal axis mirrors under RTL, against the frame the delta was
measured in.

The header cell, not the gripper, is the keyboard target: that is where column
commands belong, and one tab stop per gripper would sit between the user and the
data. The gripper is not a tab stop by default, and TryKeyboardStep owns
direction, the RTL mirror, KeyboardIncrement and the Shift multiplier so both
keyboard paths cannot drift apart.

The automation peer reports the Thumb control type -- there is no value to
expose -- composes a per-instance name from OwnerName so several grippers in one
header band are distinguishable, and returns an empty AutomationId because a
constant id on sibling elements breaks UIA sibling-uniqueness.

The primitive ships only in Microsoft.UI.Xaml.Controls.Tabular.dll and lives in
a private namespace, so it appears in no shipped WinMD.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b77872a9-8623-47b9-a9f1-2a45e3ca3ead

* ResizeGripper: review fixes - stale-drag revert, resize cursor, vertical affordance

Addresses the nine findings from the framework review of PR #11640, plus one
the review missed and running the sample found (F-13).

F-1 (High) A drag started while another was live reverted the column to a width
  from a PREVIOUS gesture. TableView's DragStarted reset didWrite/didDelta only
  after its "one resize at a time" guard, and that guard ends the new gesture
  with EndDrag(canceled) - so DragCompleted saw the previous drag's didWrite and
  restored the previous drag's startWidth, silently undoing a committed resize.
  Reachable with a pointer drag held on one gripper and an arrow key on another
  header, or with a second touch contact. The reset now happens first.

F-2 DragOrientation="Vertical" is advertised in the IDL but the separator was
  hard-wired to 0,0,2,0, so a vertical gripper drew its line along the axis it
  travels. The two concerns are now on two visual-state groups - CommonStates
  owns whether the separator shows (Opacity), the new OrientationStates owns
  which edge it is drawn on (BorderThickness) - because one group would have
  both states writing the same target. Adds
  ResizeGripperSeparatorThicknessVertical and UpdateOrientationVisualState,
  driven from OnApplyTemplate and the DragOrientation property change. The
  separator also moved into its own non-hit-testable child so showing and hiding
  it cannot interact with pointer input.

F-4 Every gripper published a named but pattern-less Thumb in the UIA Content
  view, doubling the header band's node count with nodes that are not tab stops
  and expose nothing to invoke. AccessibilityView="Raw" in the default style,
  matching ScrollBar's HorizontalThumb/VerticalThumb and SortIndicator's glyph.
  OwnerName and the composed peer name are kept - they still identify the
  element in the raw view and for a host that opts it back into Content.

F-5 TableViewResizeGripperWidth existed only in the theme-resources page, which
  is not emitted for the Tabular binary, so the key never resolved and the C++
  constant was the real default. Mirrored into TableView.xaml's root dictionary
  alongside the two metrics already mirrored there for the same reason.

F-7 A host handler that throws out of DragStarted/DragDelta left IsDragging
  stuck true on the keyboard path: unlike a pointer drag there is no
  ManipulationCompleted to follow and clean up, so every later BeginDrag
  early-returned and resize died for the life of the element. Not a
  wil::scope_exit - EndDrag raises DragCompleted, and running that from a
  noexcept guard during unwind would terminate on a handler that throws in turn.

F-8 Added the template contract block to the IDL - required parts, the named
  state target, both visual-state groups and the lightweight-styling keys -
  following the convention TableView.idl already sets.

F-9 Regenerated XamlMetadataProviderGenerated.h. It is produced by the T4 in
  Microsoft.UI.Xaml.Common.targets from the stripped winmd; this is that
  output, not a hand-written entry.

F-10 StringUtil::FormatString ignored the FormatMessage result and constructed
  an hstring from a null buffer on failure, which the automation peer's comment
  assumed could not happen - inside a UIA callback, where nothing may escape.
  The helper now returns an empty string, making the peer's fallback true.

F-12 The width announcement's translator comment said keyboard-only; it is
  raised from DragCompleted for pointer drags too.

F-13 The resize cursor never appeared - hovering a divider lit the separator
  but left the pointer an arrow, so nothing about the element said it was
  draggable. UpdateResizeCursor ran only from OnApplyTemplate and the
  DragOrientation change; a ProtectedCursor assigned before the element is live
  does not stick (microsoft-ui-xaml#7062). The function's own comment already
  said the shape "gets re-resolved while the pointer is already over us" - that
  re-resolution was simply never wired up. Now called from OnPointerEntered,
  where it no-ops if the shape is already in effect.

Built: Microsoft.UI.Xaml.Controls.Tabular.vcxproj, amd64chk, clean.

Verified by running the TableView sample (amd64chk, unpackaged self-contained):
- F-4: UIA Content view has 0 gripper nodes; the Raw view still has all 8, with
  per-column composed names.
- F-13: sweeping the header band, the west-east cursor appears over the column
  dividers. The same sweep on a build without this change produced 1603 arrow
  samples and not one resize cursor - so the fix is what makes the difference.

Still not proven by either, and deliberately left open: F-1 needs the
concurrent-drag trigger performed; the vertical affordance has no consumer that
sets DragOrientation="Vertical"; the 8 DIP touch target is unchanged pending
design sign-off.

* ResizeGripper: own the keyboard, tighten the primitive

Follow-up to the review fixes, from comparing the primitive's responsibilities
against the Community Toolkit's SizerBase - the closest shipping prior art.

Own the keyboard (was F-6, previously cut as "by design")
  The gripper held all the keyboard policy - direction, the RTL mirror, the step
  size and the Shift multiplier - but had no OnKeyDown, so a host that set
  IsTabStop="True" got a control that takes focus, shows a focus visual and
  ignores arrow keys. Policy without the matching behaviour. It now handles its
  own keys through the same TryKeyboardStep a host calls, so neither path can
  drift. TableView is unaffected: its grippers are not tab stops, so it keeps
  driving TryKeyboardStep from the header cell.
  Precedent: SizerBase overrides OnKeyDown and runs the same
  OnDragStarting/OnDragHorizontal sequence a pointer drag uses.

Shift stays a multiplier - a LargeKeyboardIncrement property was tried and cut
  An earlier revision replaced c_largeIncrementMultiplier{4.0} with an absolute
  LargeKeyboardIncrement DP. That was wrong: an absolute second value does not
  compose with the base one. A host setting KeyboardIncrement=20 got 32 (1.6x)
  instead of 80, and at 40 the Shift step collapsed to 40 (1x) against the
  std::max floor that stops it inverting outright. The multiplier is
  scale-invariant by construction; the property needed a guard to be merely
  not-wrong, and no consumer sets either knob today. SizerBase argues the same
  way - it has no modifier multiplier at all, and records that if one were added
  it would be sized relative to the base rather than as a second absolute.

One definition of the KeyboardIncrement default
  8.0 was stated twice with nothing linking them: the IDL's MUX_DEFAULT_VALUE
  (what an unset property returns) and a c_defaultKeyboardIncrement constant
  (the fallback when a host supplies 0, NaN or infinity - the "behave as if
  unset" case). Changing one would silently diverge from the other. The constant
  now lives in ResizeGripper.h and the IDL names it, so the codegen consumes the
  same symbol: BoxValueIfNecessary(c_defaultKeyboardIncrement). 8.0 appears once
  in the control. MUX_DEFAULT_VALUE already takes a C++ expression - ColorPicker
  and AnimatedVisualPlayer pass enum symbols the same way.

Stop the contract overstating itself
  The IDL called the type a "policy-free drag handle" that "owns no layout",
  while it owns its presentation, its cursor and the keyboard step. Round 1 of
  review introduced that wording when the type stopped owning a value; round 2
  then moved the visual states and theme keys into it and left the wording
  alone. It now says what it actually owns.

Tightening
  - The drag axis was derived at five sites in three phrasings, two of which
    computed the same boolean with opposite-polarity tests (!= Vertical in the
    pointer path, == Horizontal in the keyboard path). One IsHorizontalDrag()
    now serves all five - that divergence is where an axis bug hides.
  - Dropped the UpdateResizeCursor call in OnApplyTemplate; see below.

Correction to the previous commit's F-13 claim
  7ed5ccd2 said the resize cursor "appears over the column dividers", verified
  by sweeping the header band. That measurement was wrong: the sweep moves the
  pointer between samples, so it caught the shape flashing on pointer ENTRY, not
  the steady state. Dwelling on a divider without moving reads ARROW 40 times
  out of 40, and a 1px jiggle reads ARROW 20 out of 20.

  What OnPointerEntered actually buys is real but partial: without it the shape
  never appears at all (0 hits in 1603 samples on a build without it); with it
  the shape appears on entry and reverts on the next pointer move.

  Re-asserting the cursor from OnPointerMoved, forced past the same-shape guard,
  was tried and measured to change nothing - so it is not in this commit.

  Mechanism, from the framework source in this repo: the setter only pushes the
  cursor when the element is live (uielement.cpp:929), and every pointer move
  recomputes it by walking up from the hit visual to the first hit-test-visible
  FrameworkElement with IsProtectedCursorSet (InputServices.cpp:1108-1131). The
  in-box controls do not use ProtectedCursor for this at all - TextBoxView sets
  the core field directly (TextBoxView.cpp:135), which the per-move walk reads
  every time, and that field is not reachable from a lifted control. The
  shadowing theory was checked and disproved: m_eMouseCursor defaults to
  MouseCursorDefault (framework.cpp:32, EnumDefs.h:76).

  F-13 therefore stays OPEN and needs a debugger on the chk build, breaking in
  CInputServices::UpdateCursor, or a framework fix.

Built: Microsoft.UI.Xaml.Controls.Tabular.vcxproj, amd64chk, clean.

Runtime-verified on the sample (amd64chk): arrow keys step 8, Shift steps 32,
direction reverses. Not verifiable here: OnKeyDown itself, because TableView's
grippers are deliberately not tab stops and no other consumer exists.

* TableView: header rebuild and keyboard-resize lookups, BringIntoView clamp

Four fixes in the header path, from a read of the column-resize integration.

BringIntoView: clamp before the comparison, not inside ChangeView
  The handled/moved decision was made on an unclamped offset while the scroll
  used the clamped one, so a negative offset would mark the event handled and
  then not move. Not a live bug as written - the right-edge branch computes
  bounds.X + bounds.Width - viewport under a guard that puts it strictly above
  current >= 0, so only a negative bounds.X could reach it - but the asymmetry
  is free to remove and makes "did we move" and "did we handle" the same
  question.

One resource walk per rebuild, not one per column
  RebuildHeaders already hoists the header padding, grid-line brush, row
  min-height and font size for exactly this reason, then
  AppendResizeGripperVisual called LookupElementResource for the gripper width
  per column - the same class of value, the same stability window, walking the
  ancestor chain and merged dictionaries each time. Hoisted next to the others
  and passed in.

One header-text extraction per column
  GetColumnHeaderText ran twice per column: once in RebuildHeaders for the
  header cell's automation name, once in AppendResizeGripperVisual for
  OwnerName. Its own comment claimed one extraction. Computed once and passed
  in. That comment also still described the gripper's "Tag", which OwnerName
  replaced - corrected.

Keyboard resize: stop finding the header cell twice
  ResolveFocusedHeaderColumn walks up from the focused element to the header
  cell that carries the column in its Tag, then returned only the column.
  FindResizeGripperForColumn then scanned every header cell from the host to
  find that same cell again - O(columns) of try_as casts per arrow key, at key
  repeat rate. It now hands the cell back, and FindResizeGripperInCell scans
  that cell's children only.

Considered and not taken: making m_activeColumnResizeDrag a
weak_ref<ResizeGripper> instead of a shared_ptr<ColumnResizeDragState>. Nothing
outside the lambdas reads the state through it, so it would work, but the
shared_ptr has to exist for the lambdas either way - it is a clarity change with
no measurable gain, in code already reworked twice this week.

Built: Microsoft.UI.Xaml.Controls.Tabular.vcxproj, amd64chk, clean.

Runtime-verified on the sample (amd64chk):
- keyboard resize still works through the new cell-based lookup: Right +8,
  Shift+Right +32, Left -8;
- 8 grippers in the UIA raw view, each 8 DIP wide from the hoisted value, all 8
  carrying their composed OwnerName ("Name, Resize gripper", ...).
The BringIntoView clamp is reasoned and compile-checked only; reaching it needs
a negative bounds.X, which no in-tree scenario produces.

* ResizeGripper: park the resize cursor until the framework can hold it

The shape appeared as the pointer arrived at a divider and reverted on the very
next pointer move, so what shipped was a flicker rather than an affordance -
and this control already treats cursor flicker as a defect worth guarding
against (see the reassign guard in UpdateResizeCursor).

UpdateResizeCursor is left intact and correct; only its two callers are removed,
with the reasoning recorded above the function so the restore points are
obvious. Re-add the calls in OnPointerEntered and in the DragOrientation branch
of OnPropertyChanged once ProtectedCursor survives a pointer move.

Tracked as ADO 63771830, related to microsoft-ui-xaml#7062. The hover separator
still signals that the edge is draggable.

Built: Microsoft.UI.Xaml.Controls.Tabular.vcxproj, amd64chk, clean, no
unreferenced-function warning.

Runtime-verified on the sample (amd64chk):
- no resize cursor anywhere in the header band: 1820 samples, all 1820 over the
  app window, ARROW 1820/1820 - the flicker is gone;
- column resize itself is unaffected: Right +8, Shift+Right +32, Left -8, with
  focus on the header cell asserted before the keys were sent.

* Restore the ResizeGripper entry in the Tabular XAML metadata provider

Lost in the merge resolution, not by design. XamlMetadataProviderGenerated.h
conflicted; I took main's side intending to let the build regenerate it, the
build did regenerate it correctly, but the regenerated file was never staged -
"git add controls/dev/Generated" does not cover controls/dev/dll-tabular - and
the leftover modification was then misread as unrelated codegen drift.

Effect of the miss: CommonHelpers.tt still allow-listed
Microsoft.UI.Private.Controls.ResizeGripper while the checked-in provider had no
entry for it. A build regenerates the header, so this did not break the build -
but the committed state was the exact "private templated control with no
XAML metadata" shape the allow-list and its fail-the-build guard exist to
prevent, and it silently undid the F-9 fix from 7ed5ccd2.

This is the build's own T4 output: the type entry, the ResizeGripper.properties.h
include and the ClearProperties call, +46 lines, nothing else.

---------

Co-authored-by: Hitesh Kumar <hiteshkrmsft@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b77872a9-8623-47b9-a9f1-2a45e3ca3ead
2026-08-28 10:00:04 +05:30
Dipesh KumarandCopilot 4d9d6f3191 Port TableView data-shaping (sort + filter) core from internal lift (#11644)
Adds the ShapedItemsSource + TabularShaping pipeline and wires TableView,
TableViewColumn, the automation peers and TableViewSource to it, providing
sort and filter on top of the basic data-shaping infra.

Product-only port of user/dipesh/tableview/shaping-core-v2 (27 new files,
26 modified). Internal build/PipelineScripts mirroring config is
intentionally excluded. ResourceAccessor.h merged to preserve the
open-source repo's InkToolbar string defines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 22e963e3-084f-44c9-be76-8bf90278b146
2026-08-27 16:48:39 +05:30
Hitesh Kumarandreunion-maestro-bot ef280ef1b7 Merged PR 16574861: Remove Tabular/TableView source-mirroring exclusions (#11618)
Syncing content from committish 4f78352283d9ba6d2cf2ad1eacec29e199c8cc85

(cherry picked from commit 07fa7aee5e)

Co-authored-by: reunion-maestro-bot <reunion-maestro-bot@microsoft.com>
2026-08-25 14:52:46 +05:30