diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs
index 688472fc..a672d7e2 100644
--- a/src/TableView.Properties.cs
+++ b/src/TableView.Properties.cs
@@ -208,6 +208,16 @@ public partial class TableView
///
public static readonly DependencyProperty UseRightClickForColumnFilterProperty = DependencyProperty.Register(nameof(UseRightClickForColumnFilter), typeof(bool), typeof(TableView), new PropertyMetadata(false));
+ ///
+ /// Identifies the EnterKeyNavigation dependency property.
+ ///
+ public static readonly DependencyProperty EnterKeyNavigationProperty = DependencyProperty.Register(nameof(EnterKeyNavigation), typeof(TableViewEnterKeyNavigation), typeof(TableView), new PropertyMetadata(TableViewEnterKeyNavigation.Down));
+
+ ///
+ /// Identifies the ContinueEditingOnNavigation dependency property.
+ ///
+ public static readonly DependencyProperty ContinueEditingOnNavigationProperty = DependencyProperty.Register(nameof(ContinueEditingOnNavigation), typeof(bool), typeof(TableView), new PropertyMetadata(true));
+
///
/// Identifies the VerticalOffset dependency property.
///
@@ -356,6 +366,27 @@ public bool UseRightClickForColumnFilter
set => SetValue(UseRightClickForColumnFilterProperty, value);
}
+ ///
+ /// Gets or sets where the Enter key moves the current cell. The default, ,
+ /// moves to the cell below; moves to the next cell to the right.
+ ///
+ public TableViewEnterKeyNavigation EnterKeyNavigation
+ {
+ get => (TableViewEnterKeyNavigation)GetValue(EnterKeyNavigationProperty);
+ set => SetValue(EnterKeyNavigationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether Tab or Enter pressed while editing a cell begins editing the
+ /// cell it moves to. The default is . When , the edit is
+ /// committed and the next cell is only made current and selected, as in a spreadsheet.
+ ///
+ public bool ContinueEditingOnNavigation
+ {
+ get => (bool)GetValue(ContinueEditingOnNavigationProperty);
+ set => SetValue(ContinueEditingOnNavigationProperty, value);
+ }
+
///
/// Gets the collection view associated with the TableView.
///
diff --git a/src/TableView.cs b/src/TableView.cs
index e7c9ceb4..0220f2df 100644
--- a/src/TableView.cs
+++ b/src/TableView.cs
@@ -50,6 +50,7 @@ public partial class TableView : ListView
private TableViewCellSlotRange? _lastDragSelectionCellRange;
private ItemIndexRange? _lastDragSelectionRowRange;
private bool _cellStateDispatchPending;
+ private (TableViewCellSlot Slot, TaskCompletionSource Completion)? _pendingEdit;
private readonly HashSet _pendingCellStateRows = [];
private TableViewColumn? _resizingColumn;
private double _resizingOriginalWidth;
@@ -946,56 +947,13 @@ private void HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool ctrlKey
// Handle navigation keys
else if (e.Key is VirtualKey.Tab or VirtualKey.Enter)
{
- var isEditing = IsEditing;
-
- var newSlot = CurrentCellSlot ?? new();
-
- do
- {
- newSlot = GetNextSlot(newSlot, shiftKey, e.Key is VirtualKey.Enter);
-
- } while (isEditing && Columns[newSlot.Column].IsReadOnly);
-
- if (isEditing && currentCell is not null)
- {
- if (!EndCellEditing(TableViewEditAction.Commit, currentCell)) return;
-
- if (CurrentCellSlot == newSlot || GetCellFromSlot(newSlot) is not { } nextCell || !nextCell.BeginCellEditing(e))
- {
- SetIsEditing(false);
- }
- }
-
- MakeSelection(newSlot, false);
-
- e.Handled = true;
+ // A cancelled commit leaves the key unhandled, as before the move was factored out.
+ e.Handled = HandleTabOrEnter(e.Key, shiftKey, currentCell, e);
}
else if ((e.Key is VirtualKey.Left or VirtualKey.Right or VirtualKey.Up or VirtualKey.Down)
&& !IsEditing)
{
- var row = (LastSelectionUnit is TableViewSelectionUnit.Row ? CurrentRowIndex : CurrentCellSlot?.Row) ?? -1;
- var column = CurrentCellSlot?.Column ?? -1;
-
- if (row == -1 && column == -1)
- {
- row = column = 0;
- }
- else if (e.Key is VirtualKey.Left or VirtualKey.Right)
- {
- column = e.Key is VirtualKey.Left ? ctrlKey ? 0 : column - 1 : ctrlKey ? Columns.VisibleColumns.Count - 1 : column + 1;
- if (column >= Columns.VisibleColumns.Count)
- {
- column = 0;
- row++;
- }
- }
- else
- {
- row = e.Key == VirtualKey.Up ? ctrlKey ? 0 : row - 1 : ctrlKey ? Items.Count - 1 : row + 1;
- }
-
- var newSlot = new TableViewCellSlot(row, column);
- MakeSelection(newSlot, shiftKey);
+ MakeSelection(GetArrowSlot(e.Key, ctrlKey), shiftKey);
e.Handled = true;
}
else if (e.Key is VirtualKey.Home or VirtualKey.End)
@@ -1034,6 +992,184 @@ private int CalculateAvailablePageSize()
return (int)Math.Floor(availableHeight / rowHeight);
}
+ ///
+ /// Moves the current cell as Tab or Enter does. While editing, the edit is committed first; the cell
+ /// moved to is then edited too unless is .
+ ///
+ /// if committing the edit was cancelled, so nothing moved.
+ internal bool HandleTabOrEnter(VirtualKey key, bool shiftKey, TableViewCell? currentCell, RoutedEventArgs editingArgs)
+ {
+ var isEditing = IsEditing;
+
+ var newSlot = CurrentCellSlot ?? new();
+
+ do
+ {
+ newSlot = GetNextSlot(newSlot, shiftKey, key is VirtualKey.Enter && EnterKeyNavigation is TableViewEnterKeyNavigation.Down);
+
+ } while (isEditing && Columns[newSlot.Column].IsReadOnly);
+
+ if (isEditing && currentCell is not null)
+ {
+ if (!EndCellEditing(TableViewEditAction.Commit, currentCell)) return false;
+
+ if (!ContinueEditingOnNavigation || CurrentCellSlot == newSlot || GetCellFromSlot(newSlot) is not { } nextCell || !nextCell.BeginCellEditing(editingArgs))
+ {
+ SetIsEditing(false);
+ }
+ }
+
+ MakeSelection(newSlot, false);
+ return true;
+ }
+
+ ///
+ /// Commits the edit in progress, if any, and ends the edit session, leaving the edited cell current.
+ ///
+ ///
+ /// This is what Tab or Enter do to the cell being left, without moving. is
+ /// raised, and a handler that cancels it keeps the edit open.
+ ///
+ /// if no edit is in progress any more; if the
+ /// commit was cancelled.
+ public bool CommitEdit()
+ {
+ if (!IsEditing)
+ {
+ return true;
+ }
+
+ if (CurrentCellSlot is { } slot && GetCellFromSlot(slot) is { } cell)
+ {
+ // Move focus to the cell BEFORE the editing element is torn down, as the Escape path does.
+ cell.Focus(FocusState.Programmatic);
+
+ if (!EndCellEditing(TableViewEditAction.Commit, cell))
+ {
+ return false;
+ }
+ }
+
+ SetIsEditing(false);
+ return true;
+ }
+
+ ///
+ /// Moves the current cell as the given navigation key would, when no edit is in progress.
+ ///
+ ///
+ /// Supports the arrow keys, Tab and Enter (honouring ). A caller that
+ /// handles a key inside a cell editor - for example an arrow key that should commit the edit and move,
+ /// as in a spreadsheet entry mode - calls and then this.
+ ///
+ /// if the key was a supported navigation key and no edit is in progress.
+ public bool NavigateFromCurrentCell(VirtualKey key, bool shiftKey = false, bool ctrlKey = false)
+ {
+ if (IsEditing)
+ {
+ return false;
+ }
+
+ if (key is VirtualKey.Left or VirtualKey.Right or VirtualKey.Up or VirtualKey.Down)
+ {
+ MakeSelection(GetArrowSlot(key, ctrlKey), shiftKey);
+ return true;
+ }
+
+ if (key is VirtualKey.Tab or VirtualKey.Enter)
+ {
+ MakeSelection(GetNextSlot(CurrentCellSlot ?? new(), shiftKey, key is VirtualKey.Enter && EnterKeyNavigation is TableViewEnterKeyNavigation.Down), false);
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Makes the cell at the current cell and begins editing it, as pressing F2 on
+ /// that cell does.
+ ///
+ ///
+ /// This is for a cell that exposes an affordance of its own while it is not being edited - a
+ /// drop-down arrow, a picker button - and needs a single click on that affordance to open the editor.
+ /// The control starts an edit only from a double tap and from F2, and a single click is not a gesture it
+ /// can infer.
+ /// The sequence matches the existing gestures:
+ ///
+ /// - If another cell is being edited, that edit is committed first, as tapping or tabbing to a
+ /// different cell does. If committing is cancelled, no edit begins.
+ /// - The cell becomes the and is selected, as keyboard navigation to
+ /// it does, and is scrolled into view and focused.
+ /// - The edit then begins. is raised, and a handler that cancels it is
+ /// honoured.
+ ///
+ /// No edit begins for a read-only table, column or cell, for a column that draws itself through
+ /// (a double tap does not begin one either), or when the
+ /// cell is already being edited.
+ ///
+ /// The cell to edit.
+ /// A task whose result is if the cell entered edit mode; otherwise
+ /// .
+ public async Task BeginEditAsync(TableViewCellSlot slot)
+ {
+ if (IsReadOnly || !slot.IsValid(this))
+ {
+ return false;
+ }
+
+ if (IsEditing)
+ {
+ if (CurrentCellSlot == slot)
+ {
+ return false;
+ }
+
+ if (CurrentCellSlot is { } currentSlot
+ && GetCellFromSlot(currentSlot) is { } currentCell
+ && !EndCellEditing(TableViewEditAction.Commit, currentCell))
+ {
+ return false;
+ }
+
+ SetIsEditing(false);
+ }
+
+ // A later request supersedes an earlier one that has not started yet.
+ _pendingEdit?.Completion.TrySetResult(false);
+ _pendingEdit = null;
+
+ if (CurrentCellSlot == slot)
+ {
+ // Already current, so OnCurrentCellChanged will not run. A realized cell is edited directly,
+ // as F2 does, so the call completes synchronously; only a cell scrolled out of view waits.
+ return TryBeginEdit(GetCellFromSlot(slot) ?? await ScrollCellIntoView(slot));
+ }
+
+ var pending = new TaskCompletionSource();
+ _pendingEdit = (slot, pending);
+
+ MakeSelection(slot, false);
+
+ if (CurrentCellSlot != slot)
+ {
+ // Selection declined to make the cell current, so OnCurrentCellChanged will not start the edit.
+ _pendingEdit = null;
+ return false;
+ }
+
+ return await pending.Task;
+ }
+
+ ///
+ /// Begins editing if the same conditions a double tap applies allow it.
+ ///
+ private bool TryBeginEdit(TableViewCell? cell)
+ {
+ return cell is { IsReadOnly: false, Column.UseSingleElement: false }
+ && !IsEditing
+ && cell.BeginCellEditing(new RoutedEventArgs());
+ }
+
///
/// Ends the editing of a cell, committing or canceling the edit based on the specified action.
///
@@ -1256,6 +1392,35 @@ private void OnScrollContentPresenterPointerWheelChanged(object sender, PointerR
}
}
+ ///
+ /// Gets the slot an arrow key moves the current cell to.
+ ///
+ private TableViewCellSlot GetArrowSlot(VirtualKey key, bool ctrlKey)
+ {
+ var row = (LastSelectionUnit is TableViewSelectionUnit.Row ? CurrentRowIndex : CurrentCellSlot?.Row) ?? -1;
+ var column = CurrentCellSlot?.Column ?? -1;
+
+ if (row == -1 && column == -1)
+ {
+ row = column = 0;
+ }
+ else if (key is VirtualKey.Left or VirtualKey.Right)
+ {
+ column = key is VirtualKey.Left ? ctrlKey ? 0 : column - 1 : ctrlKey ? Columns.VisibleColumns.Count - 1 : column + 1;
+ if (column >= Columns.VisibleColumns.Count)
+ {
+ column = 0;
+ row++;
+ }
+ }
+ else
+ {
+ row = key == VirtualKey.Up ? ctrlKey ? 0 : row - 1 : ctrlKey ? Items.Count - 1 : row + 1;
+ }
+
+ return new TableViewCellSlot(row, column);
+ }
+
///
/// Gets the next cell slot based on the current slot and input keys.
///
@@ -2181,6 +2346,15 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel
var cell = await ScrollCellIntoView(newSlot.Value);
cell?.ApplyCurrentCellState();
cell?.Focus(FocusState.Programmatic);
+
+ // A BeginEditAsync call that moved the current cell starts its edit here, once the cell has
+ // been scrolled into view and focused, so that the editing element is the last thing to take
+ // focus rather than racing the Focus call above.
+ if (_pendingEdit is { } pending && pending.Slot == newSlot.Value)
+ {
+ _pendingEdit = null;
+ pending.Completion.TrySetResult(CurrentCellSlot == newSlot && TryBeginEdit(cell));
+ }
}
}
diff --git a/src/TableViewEnterKeyNavigation.cs b/src/TableViewEnterKeyNavigation.cs
new file mode 100644
index 00000000..1681c9cd
--- /dev/null
+++ b/src/TableViewEnterKeyNavigation.cs
@@ -0,0 +1,18 @@
+namespace WinUI.TableView;
+
+///
+/// Specifies where the Enter key moves the current cell.
+///
+public enum TableViewEnterKeyNavigation
+{
+ ///
+ /// Enter moves to the cell below; Shift+Enter to the cell above. This is the default.
+ ///
+ Down,
+
+ ///
+ /// Enter moves to the next cell to the right, wrapping to the first cell of the next row;
+ /// Shift+Enter moves left. This is how the gINT data-entry grid behaves.
+ ///
+ Right
+}
diff --git a/tests/TableViewBeginEditTests.cs b/tests/TableViewBeginEditTests.cs
new file mode 100644
index 00000000..fa4cb488
--- /dev/null
+++ b/tests/TableViewBeginEditTests.cs
@@ -0,0 +1,162 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+using System.Threading.Tasks;
+
+namespace WinUI.TableView.Tests;
+
+[TestClass]
+public class TableViewBeginEditTests
+{
+ [UITestMethod]
+ public async Task BeginEditAsync_MakesTheCellCurrentAndBeginsEditingIt()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(1, 1);
+
+ var began = await tableView.BeginEditAsync(slot);
+
+ Assert.IsTrue(began);
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ Assert.IsInstanceOfType(tableView.GetCellFromSlot(slot)!.Content);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_OnTheCurrentCell_BeginsEditingIt()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(0, 1);
+ tableView.CurrentCellSlot = slot;
+ await Task.Delay(100);
+
+ var began = await tableView.BeginEditAsync(slot);
+
+ Assert.IsTrue(began);
+ Assert.IsTrue(tableView.IsEditing);
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_OnTheCurrentVisibleCell_CompletesSynchronously()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(1, 0);
+ tableView.CurrentCellSlot = slot;
+ await Task.Delay(100);
+
+ // A caller handling a key or character event must know before its handler returns whether
+ // the edit began, as it does for F2; waiting on a scroll here would make that impossible.
+ var begin = tableView.BeginEditAsync(slot);
+
+ Assert.IsTrue(begin.IsCompleted, "BeginEditAsync on the current, realized cell did not complete synchronously.");
+ Assert.IsTrue(await begin);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_CommitsTheEditInProgressOnAnotherCell()
+ {
+ var tableView = await CreateTableViewAsync();
+ var ended = new System.Collections.Generic.List();
+ tableView.CellEditEnded += (_, e) => ended.Add(e.EditAction);
+
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(1, 1)));
+
+ CollectionAssert.AreEqual(new[] { TableViewEditAction.Commit }, ended);
+ Assert.AreEqual(new TableViewCellSlot(1, 1), tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_OnTheCellAlreadyBeingEdited_ReturnsFalseAndKeepsEditing()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(0, 0);
+ Assert.IsTrue(await tableView.BeginEditAsync(slot));
+
+ Assert.IsFalse(await tableView.BeginEditAsync(slot));
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_HonoursACancelledBeginningEdit()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.BeginningEdit += (_, e) => e.Cancel = true;
+
+ var began = await tableView.BeginEditAsync(new TableViewCellSlot(1, 0));
+
+ Assert.IsFalse(began);
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_DoesNotEditAReadOnlyColumn()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.Columns[1].IsReadOnly = true;
+
+ var began = await tableView.BeginEditAsync(new TableViewCellSlot(0, 1));
+
+ Assert.IsFalse(began);
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_DoesNotEditAReadOnlyTable()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.IsReadOnly = true;
+
+ Assert.IsFalse(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_RejectsASlotOutsideTheTable()
+ {
+ var tableView = await CreateTableViewAsync();
+
+ Assert.IsFalse(await tableView.BeginEditAsync(new TableViewCellSlot(5, 0)));
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ private static async Task CreateTableViewAsync()
+ {
+ var tableView = new TableView
+ {
+ SelectionMode = ListViewSelectionMode.Extended,
+ SelectionUnit = TableViewSelectionUnit.Cell,
+ };
+
+ tableView.Columns.Add(new TableViewTextColumn
+ {
+ Header = "Name",
+ Binding = new Binding { Path = new PropertyPath(nameof(EditItem.Name)), Mode = BindingMode.TwoWay }
+ });
+ tableView.Columns.Add(new TableViewTextColumn
+ {
+ Header = "Note",
+ Binding = new Binding { Path = new PropertyPath(nameof(EditItem.Note)), Mode = BindingMode.TwoWay }
+ });
+ tableView.ItemsSource = new[]
+ {
+ new EditItem { Name = "A", Note = "first" },
+ new EditItem { Name = "B", Note = "second" }
+ };
+
+ await UnitTestApp.Current.MainWindow.LoadTestContentAsync(tableView);
+
+ return tableView;
+ }
+
+ public sealed class EditItem
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Note { get; set; } = string.Empty;
+ }
+}
diff --git a/tests/TableViewKeyboardNavigationTests.cs b/tests/TableViewKeyboardNavigationTests.cs
new file mode 100644
index 00000000..ab811a6c
--- /dev/null
+++ b/tests/TableViewKeyboardNavigationTests.cs
@@ -0,0 +1,242 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Windows.System;
+
+namespace WinUI.TableView.Tests;
+
+[TestClass]
+public class TableViewKeyboardNavigationTests
+{
+ [UITestMethod]
+ public async Task Enter_moves_down_by_default()
+ {
+ var tableView = await CreateTableViewAsync();
+ await MakeCurrentAsync(tableView, new TableViewCellSlot(0, 0));
+
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Enter));
+
+ Assert.AreEqual(new TableViewCellSlot(1, 0), tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task Enter_moves_right_when_EnterKeyNavigation_is_Right()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.EnterKeyNavigation = TableViewEnterKeyNavigation.Right;
+ await MakeCurrentAsync(tableView, new TableViewCellSlot(0, 0));
+
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Enter));
+
+ Assert.AreEqual(new TableViewCellSlot(0, 1), tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task Enter_moving_right_wraps_from_the_last_column_to_the_first_cell_of_the_next_row()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.EnterKeyNavigation = TableViewEnterKeyNavigation.Right;
+ await MakeCurrentAsync(tableView, new TableViewCellSlot(0, 2));
+
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Enter));
+
+ Assert.AreEqual(new TableViewCellSlot(1, 0), tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task Shift_Enter_moving_right_goes_left()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.EnterKeyNavigation = TableViewEnterKeyNavigation.Right;
+ await MakeCurrentAsync(tableView, new TableViewCellSlot(1, 1));
+
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Enter, shiftKey: true));
+
+ Assert.AreEqual(new TableViewCellSlot(1, 0), tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task Right_arrow_and_Tab_move_to_the_next_cell_to_the_right()
+ {
+ var tableView = await CreateTableViewAsync();
+ await MakeCurrentAsync(tableView, new TableViewCellSlot(0, 0));
+
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Right));
+ Assert.AreEqual(new TableViewCellSlot(0, 1), tableView.CurrentCellSlot);
+
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Tab));
+ Assert.AreEqual(new TableViewCellSlot(0, 2), tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task CommitEdit_commits_and_ends_the_edit_leaving_the_cell_current()
+ {
+ var tableView = await CreateTableViewAsync();
+ var ended = new List();
+ tableView.CellEditEnded += (_, e) => ended.Add(e.EditAction);
+ var slot = new TableViewCellSlot(1, 1);
+ Assert.IsTrue(await tableView.BeginEditAsync(slot));
+
+ Assert.IsTrue(tableView.CommitEdit());
+
+ Assert.IsFalse(tableView.IsEditing);
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ CollectionAssert.AreEqual(new[] { TableViewEditAction.Commit }, ended);
+ }
+
+ [UITestMethod]
+ public async Task CommitEdit_keeps_editing_when_the_commit_is_cancelled()
+ {
+ var tableView = await CreateTableViewAsync();
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+ tableView.CellEditEnding += (_, e) => e.Cancel = true;
+
+ Assert.IsFalse(tableView.CommitEdit());
+
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task CommitEdit_with_no_edit_in_progress_returns_true()
+ {
+ var tableView = await CreateTableViewAsync();
+
+ Assert.IsTrue(tableView.CommitEdit());
+ }
+
+ [UITestMethod]
+ public async Task NavigateFromCurrentCell_is_refused_while_editing()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(0, 0);
+ Assert.IsTrue(await tableView.BeginEditAsync(slot));
+
+ Assert.IsFalse(tableView.NavigateFromCurrentCell(VirtualKey.Right));
+
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task Commit_then_navigate_moves_right_like_entry_mode()
+ {
+ var tableView = await CreateTableViewAsync();
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+
+ Assert.IsTrue(tableView.CommitEdit());
+ Assert.IsTrue(tableView.NavigateFromCurrentCell(VirtualKey.Right));
+
+ Assert.AreEqual(new TableViewCellSlot(0, 1), tableView.CurrentCellSlot);
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task Enter_while_editing_edits_the_next_cell_by_default()
+ {
+ var tableView = await CreateTableViewAsync();
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+
+ Assert.IsTrue(tableView.HandleTabOrEnter(VirtualKey.Enter, false, CurrentCell(tableView), new RoutedEventArgs()));
+
+ Assert.AreEqual(new TableViewCellSlot(1, 0), tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task Enter_while_editing_commits_and_only_selects_the_cell_to_the_right_when_not_continuing()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.EnterKeyNavigation = TableViewEnterKeyNavigation.Right;
+ tableView.ContinueEditingOnNavigation = false;
+ var ended = new List();
+ tableView.CellEditEnded += (_, e) => ended.Add(e.EditAction);
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+
+ Assert.IsTrue(tableView.HandleTabOrEnter(VirtualKey.Enter, false, CurrentCell(tableView), new RoutedEventArgs()));
+
+ Assert.AreEqual(new TableViewCellSlot(0, 1), tableView.CurrentCellSlot);
+ Assert.IsFalse(tableView.IsEditing);
+ CollectionAssert.AreEqual(new[] { TableViewEditAction.Commit }, ended);
+ }
+
+ [UITestMethod]
+ public async Task Tab_while_editing_only_selects_the_next_cell_when_not_continuing()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.ContinueEditingOnNavigation = false;
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(1, 2)));
+
+ Assert.IsTrue(tableView.HandleTabOrEnter(VirtualKey.Tab, false, CurrentCell(tableView), new RoutedEventArgs()));
+
+ Assert.AreEqual(new TableViewCellSlot(2, 0), tableView.CurrentCellSlot);
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task Enter_while_editing_moves_nothing_when_the_commit_is_cancelled()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(0, 0);
+ Assert.IsTrue(await tableView.BeginEditAsync(slot));
+ tableView.CellEditEnding += (_, e) => e.Cancel = true;
+
+ Assert.IsFalse(tableView.HandleTabOrEnter(VirtualKey.Enter, false, CurrentCell(tableView), new RoutedEventArgs()));
+
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ private static TableViewCell? CurrentCell(TableView tableView)
+ {
+ return tableView.CurrentCellSlot is { } slot ? tableView.GetCellFromSlot(slot) : null;
+ }
+
+ private static async Task MakeCurrentAsync(TableView tableView, TableViewCellSlot slot)
+ {
+ tableView.MakeSelection(slot, false);
+ await Task.Delay(100);
+ }
+
+ private static async Task CreateTableViewAsync()
+ {
+ var tableView = new TableView
+ {
+ // Exactly the three columns declared below; the default would add three generated ones, and the wrap
+ // test's "last column" would then not be the last (CI run 36832285874: landed on column 3 of 6).
+ AutoGenerateColumns = false,
+ SelectionMode = ListViewSelectionMode.Extended,
+ SelectionUnit = TableViewSelectionUnit.Cell,
+ };
+
+ foreach (var name in new[] { nameof(NavItem.A), nameof(NavItem.B), nameof(NavItem.C) })
+ {
+ tableView.Columns.Add(new TableViewTextColumn
+ {
+ Header = name,
+ Binding = new Binding { Path = new PropertyPath(name), Mode = BindingMode.TwoWay }
+ });
+ }
+
+ tableView.ItemsSource = new[]
+ {
+ new NavItem { A = "a0", B = "b0", C = "c0" },
+ new NavItem { A = "a1", B = "b1", C = "c1" },
+ new NavItem { A = "a2", B = "b2", C = "c2" },
+ };
+
+ await UnitTestApp.Current.MainWindow.LoadTestContentAsync(tableView);
+
+ return tableView;
+ }
+
+ public sealed class NavItem
+ {
+ public string A { get; set; } = string.Empty;
+ public string B { get; set; } = string.Empty;
+ public string C { get; set; } = string.Empty;
+ }
+}