Tabular: add opt-in per-cell tooltips to TableView - #11669
Draft
Hitesh Kumar (hiteshkrmsft) wants to merge 6 commits into
Draft
Tabular: add opt-in per-cell tooltips to TableView#11669Hitesh Kumar (hiteshkrmsft) wants to merge 6 commits into
Hitesh Kumar (hiteshkrmsft) wants to merge 6 commits into
Conversation
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. Accessibility: the tooltip text is published as the cell's AutomationProperties.HelpText unless the cell already reports that text, so assistive technology receives the information exactly once. When Content is not a string, the handler sets ToolTipHelpText to supply the accessible equivalent - the cell wrapper the tooltip is attached to is internal, so an app cannot set HelpText on it directly. Recycling: the control tracks the tooltip it attached and the exact HelpText it published, so a recycled row never shows or announces a previous item's tooltip, including after the last handler is removed. InvalidateCellToolTips() re-resolves every realized cell for handlers attached after realization, or when the data behind the tooltips changes; it is coalesced onto the dispatcher so it is safe to call from inside a handler. 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. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Contain handler exceptions per row on the deferred invalidate path, which previously let a throwing handler escape into a dispatcher callback. Drain a handler-queued rebuild from refreshes reached directly (invalidate, edit close); only the rebuild path drained, leaving those rows stamped for the previous state. Test ownership against the raw attached value: ToolTipService stores whatever the app set, and a bare string was read as an empty slot and overwritten. Reject a ToolTip as Content rather than nesting it, and correct the IDL, which claimed it overrides placement. Keep the invalidate queued flag set across the pass and record a follow-up, so a handler calling it cannot queue a callback every tick. Release tooltip content from OnRowElementClearing: the release path was unreachable because recycle-out never rebuilds, so app content stayed alive in the pool. Retract the tooltip when an edit begins and re-resolve when one is abandoned, covering forced closes that do not rebuild. Keep the owned-tooltip flag true unless the pass completed, so a handler throwing on the first cell cannot hide earlier tooltips from cleanup. Resolve the cell's UIA text only when there is something to publish; it can allocate an automation peer.
The rebuild drain called the public RefreshCellToolTips, which drains as well, so a handler that dirtied the row started a nested drain with a fresh counter instead of tripping the cap. Depth was bounded only by how long the app kept mutating, and the "dropping a pending cell rebuild" diagnostic was unreachable on the path it was written for. Both loops now run over the Core primitives and share one pass cap, so a handler gets the same number of replays whichever entry point it reaches. Bound the deferred invalidate the same way. A handler calling InvalidateCellToolTips on every raise re-armed the follow-up forever, queuing a dispatcher callback per tick and consuming a core; beyond the cap the request is now dropped with a diagnostic. Hold the coalescing flag across the synchronous fallbacks, which ran without it, so a handler reaching them re-entered and recursed rather than recording a follow-up. Clear the flag on the throw path too: it was a bare assignment, and a pass that threw left every later invalidate coalescing into a callback that would never run. Raise CellToolTipRequested inside the try that applies its result. Only the apply was contained, so a throwing handler escaped through the edit-close paths, which run from key and focus callbacks. Test the cheap conditions before resolving the cell's UIA text, which can allocate an automation peer for non-text content, and correct the IDL: calling the invalidate from a handler is re-entrancy-safe, but it is not free to call unconditionally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename CellToolTipRequestedEventArgs.ToolTipHelpText to AutomationHelpText. The value is published verbatim as the cell's UIA HelpText, and the old name read as help text belonging to the tooltip. Resolve the cell's UIA text without creating an automation peer on the tooltip path. The duplicate-suppression comparison ran on every realization and every invalidate, so a template column allocated a peer per cell with no UIA listener attached. Refresh tooltips once more when a drain runs out of passes, so cells rebuilt on the last pass do not keep the previous item's tooltips, and the drop diagnostic can be reached. Make the synchronous invalidate fallback terminal: it now drops a handler-requested follow-up with a diagnostic rather than silently, and resets the pass counter so a later invalidate is not capped early. Correct two IDL claims: the accessible text is suppressed when the cell already reports it, and the invalidate does run synchronously when there is no dispatcher. Document the tooltip accessibility contract in the API spec, record that the event is also raised when a cell edit closes, and state that content returned as a UIElement is parented by that cell's tooltip and cannot be shared across cells. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d-edits Non-string tooltip content with no AutomationHelpText reaches assistive technology through nothing but the popup. The control does not synthesize the text - no XAML control derives HelpText from a content peer, and a panel's peer name is empty in the common case - so it reports the condition instead of failing silently. Logged once per pass rather than per cell, since a whole column would otherwise log on every scroll. Mark the two hand-edited lines in the Tabular metadata provider template, which a future sync from the MUXC template would otherwise drop silently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves additive conflicts with the TableView data-shaping port (#11644) and the ResizeGripper primitive (#11640): both sides' entries are kept in TableView.vcxitems, the Sorting and Tooltips IDL blocks sit side by side, and the tooltip event-args forward declaration joins the shaping helpers' in TableView.h.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an opt-in per-cell tooltip to
TableView.Text cells render with
CharacterEllipsisand no wrapping, so a value wider than its column is unreadable and an app has no way to surface the full text.CellToolTipRequestedgives it one: the handler receives the row'sItemand the cell'sColumn, and setsContent.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.
API
[MUX_PREVIEW][webhosthidden] runtimeclass TableViewCellToolTipRequestedEventArgs { Object Item { get; }; MU_XC_NAMESPACE.TableViewColumn Column { get; }; Object Content { get; set; }; String ToolTipHelpText { get; set; }; }; // on TableView event TypedEventHandler<TableView, TableViewCellToolTipRequestedEventArgs> CellToolTipRequested; void InvalidateCellToolTips();Design
The control owns the
ToolTip.Contentis the tooltip's content, not aToolTipto attach.ToolTipServicerebinds aToolTip's owner and container on every registration, so one instance handed back for several cells would leave them contending for a single owner slot and one set of event tokens. The control creates the object and setsPlacementMode.Mouse, since cells are arbitrarily wide and element-relative placement can land the tip far from the pointer.Ownership is recorded, not inferred. A private attached record holds the
ToolTipthe control attached and the exactHelpTextit published, compared against what is actually on the element. A tooltip the app replaces is detected and left alone; the control only ever touches what it put there. Recording before attaching means a failure degrades to "not ours" rather than orphaning a tooltip nothing can clear.Recycling. A recycled row never shows or announces a previous item's tooltip, including after the last handler is removed — retraction does not depend on a handler still being attached. A row recycled out of view releases app-supplied content rather than pinning it in the repeater's pool. Control-created tooltips are neutralized in place rather than detached, so scrolling reuses the object instead of allocating a
Controlper cell per scroll, matchingTabViewItem.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. A rebuild triggered from a handler is recorded and replayed after the pass rather than running underneath it.
InvalidateCellToolTips()is coalesced onto the dispatcher rather than raising synchronously, so it is safe to call from inside a handler.Accessibility
The tooltip text is published as the cell's
AutomationProperties.HelpTextunless the cell already reports that text, so the information reaches assistive technology exactly once instead of being pointer-only or announced twice. The comparison uses the same cell-text helperTableViewCellAutomationPeeruses to compose its name, so the two cannot drift.When
Contentis not a string it cannot be stringified, and the cell wrapper the tooltip attaches to is internal — an app cannot setHelpTexton it.ToolTipHelpTextis the supported way to supply the accessible equivalent.The popup itself is pointer-only: cell focus in
TableViewis row-level, so there is no cell element for the framework's keyboard-tooltip path to fire on. The UIA pairing is what serves keyboard and screen-reader users, and that is why it is not optional.Deferred
Column-header and group-header tooltips.
PART_HeaderScrollerisIsHitTestVisible="False"and hosts every header cell, and it is scrolled programmatically from the body — a header tooltip could never open, and making the band hit-testable is template work beyond this change. Grouping is not wired intoTableViewonmainyet. Both surfaces land with the work that makes them interactive.Notes for reviewers
Generated files.
controls/dev/Generated/TableView.properties.*andcontrols/dev/dll-tabular/XamlMetadataProviderGenerated.hare codegen output and are regenerated here rather than hand-edited. The regeneration also picks upGroupHeaderTemplateand the group-header/group-info entries, which are already declared in the IDL but had not been regenerated into the checked-in output — that churn is not from this change. The generated header is committed rather than left to the incremental codegen target, whoseInputs/Outputstimestamp comparison can be skipped on a fresh clone.Tooltip property teardown. The ownership property is registered lazily, outside the generated
TableViewProperties, soXamlMetadataProviderGenerated.ttnow emits its clear call intoClearTypeProperties()and emits its own#include. Without it a XAML re-initialization would keep handing out a registration from the torn-down core. Onlycontrols/dev/dll-tabularis affected; the MUXC provider has its own template and does not host TableView.Testing. There is no
TableViewtest project in the repo yet, so this change ships without automated coverage; the behavioural contract is written down indocs/design-notes/TabularControls/TableView-functional-spec.mdand the API indocs/api-specs/TableView/TableView-spec.mdso tests have something to bind to. The control DLL builds clean, but the tooltip behaviour has not been exercised at runtime — worth weighting in review, particularly the recycling and UIA-pairing paths.