Skip to content

Feature/task list keyboard project management - #2176

Open
thisisthedave wants to merge 32 commits into
callumalpass:mainfrom
thisisthedave:feature/task-list-keyboard-project-management
Open

Feature/task list keyboard project management#2176
thisisthedave wants to merge 32 commits into
callumalpass:mainfrom
thisisthedave:feature/task-list-keyboard-project-management

Conversation

@thisisthedave

Copy link
Copy Markdown

Summary

This PR improves Task List interaction through configurable
keyboard navigation, persistent focus and selection, batch editing, and richer
grouped-property drag-and-drop behavior.

It also adds project-assignment editing, normalizes list-valued group headings,
and introduces settings for controlling whether drops move or copy values
between groups.

New features

Keyboard navigation and focus

  • Added keyboard navigation through Task List cards.

  • Default navigation shortcuts now include:

    • Next task: ArrowDown or j
    • Previous task: ArrowUp or k
    • First task: Home
    • Last task: End
  • Moving the mouse over a task transfers focus to that card.

  • Keyboard focus is restored when:

    • The Task List is first opened.
    • Its tab is brought back to the foreground.
    • The active tab is clicked again.
    • Search is submitted or cancelled.
    • A task edit causes the Bases view to rerender.
  • Logical task focus is tracked independently from transient DOM elements,
    preventing rerenders from leaving focus on document.body.

  • Focus and selection have distinct, consistent styling:

    • Focused tasks use a gray border.
    • Selected tasks use a purple background and dim purple border.
    • The first selected task receives the brighter selection border.
    • Selection styling takes precedence over the focus background, while the
      focus border remains visible.

Task selection and batch actions

  • Added configurable shortcuts for:
    • Toggling selection: Space or x
    • Selecting all visible tasks: Ctrl+A / Cmd+A
    • Clearing focus and selection: Escape or Backspace
    • Copying selected task titles: Ctrl+C / Cmd+C
    • Toggling archive state: y
task-keyboard-nav
  • Existing task-editing shortcuts now operate on the active selection.

  • Batch operations only affect tasks that are both selected and currently
    visible. Tasks hidden by search or filtering remain selected but are not
    edited.

  • Selection is preserved across:

    • Task edits
    • Bases rerenders
    • Tab changes
    • Search activation and cancellation
    • Context-popup cancellation
  • Pressing Escape inside a contextual popup or search field no longer
    unintentionally clears the Task List selection.

  • Clearing selection leaves the Task List ready for continued keyboard
    navigation.

Configurable Task List shortcuts

  • Added a dedicated settings interface for Task List shortcuts.
  • Shortcuts support arbitrary modifier combinations and multiple bindings per
    action.
image
  • Task List shortcuts use an Obsidian child scope while the view is active,
    allowing them to work even when the same chord is assigned to a global editor
    command.

  • Added duplicate detection with cancel and replace options.

  • Shortcut capture now includes an explicit confirm/cancel step.

  • Escape can be assigned as a shortcut without dismissing the underlying
    Settings dialog.

  • Shortcut bindings can be individually removed using a circle-x action.

  • New bindings are added using a circle-plus action.

  • Each action can be reset to its defaults.

Search integration

  • / focuses the Task List search field.
  • Pressing Enter in search returns keyboard focus to the task list.
  • Pressing Escape exits search without clearing the current selection.
  • Task navigation and edit shortcuts continue to work after leaving search.

Project assignment editing

  • The project-selection dialog now supports removing existing project
    assignments as well as adding new ones.
image
  • The project selection dialog and drag-and-drop code now use shared code to assign projects so that both write wikilink-style values into the projects property.

Grouped-property drag and drop

Drag and drop between groups now works generically for list-valued properties,
including projects, tags, contexts, and compatible custom list properties.

image task-drag-drop

Three configurable behaviors are available under Appearance & UI → Task
Interaction:

  • Move between groups — replace the source group values with the destination
    values.

  • Copy between groups — preserve existing values and add the destination values.

  • Move; Ctrl/Option to copy — move by default, or preserve existing values when
    the modifier is held. This is the default.

Additional behavior includes:

  • Dropping a selected task applies the operation to all selected, visible tasks.
  • Each task’s source values are evaluated independently during a batch drop.
  • Dropping into an ungrouped section clears the applicable property when using
    move behavior.

Stable grouping for list properties

List-valued group keys are normalized before being submitted to the Bases
renderer.

For example, these project assignments now appear in the same group:

  • [[Kitchen]], [[Baking]]
  • [[Baking]], [[Kitchen]]
image

Normalization affects grouping identity only and does not rewrite the order
stored in task frontmatter.

Implementation hotspots

Changes Source Location
Main Task List integration, lifecycle, keyboard dispatch, batch action targeting, and drag/drop TaskListView.ts
Logical task focus and focus restoration across rerender TaskListFocusController.ts
Determining when the Task List owns keyboard input TaskListInputOwnershipController.ts
Shortcut definitions, defaults, matching, and action metadata taskListKeyboardActions.ts
Generic list-property drop behavior and batch drop planning taskListDropPlanning.ts
Order-insensitive normalization of list-valued groups taskListGrouping.ts
Conversion of Bases list group key basesValueConversion.ts
Shortcut settings UI, capture scope, confirmation, and duplicate handling keyboardShortcutsTab.ts
Group drag/drop behavior setting appearanceTab.ts
Default settings and shortcut bindings defaults.ts
Settings types and drop-behavior definitions settings.ts
Settings loading and shortcut normalization settingsPersistence.ts
Adding and removing project assignments ProjectSelectModal.ts
Canonical project-reference generation linkUtils.ts
Focus, selection, shortcut, grouping, and drop styling styles.css

Testing

The PR adds focused unit coverage for:

  • Keyboard shortcut matching and configurable bindings
  • Focus and input ownership
  • Selection persistence and visible-only batch targeting
  • Shortcut settings persistence
  • List-property drop planning
  • Project-reference generation
  • List-valued group normalization

- Task-list shortcuts pause while an Obsidian menu or modal is open.
- Menu/modal Escape and Backspace events remain owned by the overlay.
- Focus returns to the originating task after an overlay closes.
- Focus is not stolen if the user intentionally moves to another control.
- Escape clears task selection without moving focus to the Bases root.
- Escape dismisses search; Backspace dismisses an already-empty search and returns to the focused task.
- IME composition and editable controls suppress task-list shortcuts.
- All listeners and pending restoration timers are cleaned up with the view.
  - Selection/focus visuals now have deterministic precedence:
      - Purple selection background overrides gray focus background.
      - Gray focus border overrides the primary-selection border.
      - Every selected task gets a subdued purple border.
      - The primary selected task retains the brighter purple border
        when not focused.

  - The first rendered task receives keyboard focus when the task view
    initially loads, so shortcuts work immediately without clicking a
    task first.
  - Selection state is rehydrated whenever Bases rebuilds the view
    after an edit.

  - Batch property, tag, and archive edits no longer explicitly clear
    selection. Delete and “Clear selection” still do.

  - Task-list views listen for workspace leaf activation and restore
    the previously focused card when their tab returns to the
    foreground.
  - Keyboard shortcuts now remain available when focus is on a non-
    interactive part of the foreground task view. Actions use the
    remembered focused card as their target.

  - Cancelling search restores card focus, and subsequent rerenders
    continue restoring it.

  - / now creates and opens the search controls when search was
    disabled for the Bases view, then focuses the input.

  - Existing search controls are simply focused.
  Keyboard multi-edit actions now:

  - Target only selected tasks present in the current filtered view.
  - Preserve hidden tasks in the selection state.
  - Do nothing if every selected task is filtered out.
  - Continue using the focused task when there is no active selection.
  - Enter in search preserves the query and returns focus to the remembered task.
  - Added customizable actions:
      - Clear focus/selection: Escape, Backspace
      - Toggle selection: Space
      - Copy selected/focused visible task titles: Ctrl/Cmd+C
      - Select all visible tasks: Ctrl/Cmd+A
      - Toggle archive: Y

  - Legacy shortcut settings migrate to the new action names.
  - Mixed archived/unarchived selections show a warning instead of toggling unpredictably.
  - Shortcut conflict detection and replace/cancel behavior automatically covers the new actions.
  - Fixed literal Space key normalization.
  The active Task List now pushes a child keyboard scope containing its configured shortcuts. This gives  bindings such as Ctrl+B and Ctrl+D precedence over global Bold/Delete Paragraph commands while:

  - The Task List leaf is active.
  - Keyboard focus belongs to a non-editable part of that view.
  - No menu, modal, search input, or editor owns input.

  The scope is popped when the leaf deactivates, focus enters an editable control, or the view unloads.
- Hotkey recording now pushes a temporary Obsidian Scope that consumes Escape, cancels recording, and
    prevents the underlying Settings dialog from closing. Cleanup is idempotent and also runs during
    rerenders.
  - Escape is now captured as a normal shortcut and forwarded to the confirmation flow without closing
    Settings.

  - New shortcuts require confirmation before being saved.
  - “Add shortcut” is now a circle-plus icon.
  - Reset appears before Add.
  - Existing bindings display a circle-x icon beside the value; it turns red on hover.
  - Added scoped styling and updated regression coverage.
  - Moving the mouse over a task updates its visual/remembered focus without stealing DOM focus.
  - Moving over empty list space leaves focus unchanged.
  - Selection toggle targets the mouse-focused task.
  - Navigation shortcuts continue from the hovered task, then switch back to normal keyboard/DOM focus.
  - Uses one delegated mousemove listener, compatible with rerenders and virtualized cards.
  - Mouse hover now moves actual card focus, clearing stale gray focus styling from the previous card.
  - Keyboard navigation switches the list to keyboard-cursor mode and suppresses lingering :hover
    backgrounds on non-focused cards.

  - Selected-task purple styling remains unaffected.
  - Edit overlays now schedule focus restoration when opened and continue monitoring until closed.
  - After task rerenders, focus returns to the replacement card, reactivating shortcut ownership while
    preserving selection.

  - Active interactive controls inside cards are protected from hover stealing focus.
- Assigned projects appear in the selector.
- Projects can be removed from the active task selection.
  - Dragging a task between project groups now replaces all existing project
    assignments with the destination project.

  - Other list grouping behavior remains unchanged.
  - File-link grouping is unaffected.
  - Added tests for frontmatter mutation and post-drop task state.
  - drag between project groups replaces all projects.
  - shift+drag adds the destination project while preserving every existing project.

  - drag between tag groups replaces all tags.
  - shift-drag adds the destination tag while preserving every existing tag.

  The Appearance & UI → Task Interaction group now includes “Project and tag drop
  behavior” with:
  - Dropping replaces values
  - Dropping adds values
  - Dropping replaces; Shift-drop adds — default
 - The setting now applies to every list-valued grouping property, including
    contexts, aliases, and custom Obsidian list properties.

  - Renamed the setting to “List-property drop behavior” and generalized its help
    text.

  - Replacement mode now replaces the entire list for any list property.
  - Additive mode preserves all existing values and adds the destination.
  - Changed native drag effects from move-only to allow all effects. Additive
    drops advertise copy; replacement drops advertise move. This permits modifier
    keys during drag initiation as intended by the HTML drag API. MDN drag
    operations
- Project-group drops now resolve the project file and generate the same canonical project reference used by the task modal.
- Respects the useFrontmatterMarkdownLinks setting; otherwise writes wikilinks
    such as "[[Project Name]]".
  - Dragging a selected task moves all selected tasks that are currently visible.
  - Filtered-out selected tasks are not modified.
  - Dragging an unselected task moves only that task.
  - Every affected task receives the destination group mutation and property-
    change side effects.

  - The card under the pointer determines the insertion position.
  - Selection remains intact after the drop.
  - Lexically sorts values when forming list-valued group identities.
  - Merges equivalent groups before rendering.
  - Handles projects, tags, contexts, and custom list properties.
  - Does not modify the order stored in task frontmatter.
  - Leaves scalar grouping unchanged.
  - Navigate down: ArrowDown, j
  - Navigate up: ArrowUp, k
  - Toggle selection: Space, x
Comment on lines +77 to +95
export function convertBasesListGroupKeyToString(key: unknown): string {
if (key === null || key === undefined) {
return convertBasesGroupKeyToString(key);
}

const basesKey = key as BasesValueInternals;
const actualValue = extractBasesGroupKeyValue(basesKey);
if (!Array.isArray(actualValue)) {
return formatBasesGroupKeyValue(actualValue);
}

const sortedValues = actualValue.map(stringifyUnknown).sort((left, right) => {
if (left < right) return -1;
if (left > right) return 1;
return 0;
});
return sortedValues.length > 0 ? sortedValues.join(", ") : "None";
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalizing list-value fields by sorting makes the task view groups insensitive to list order. In other words, a task with projects "A, B" is grouped together with a task in projects "B, A".

Comment on lines +122 to +127
if (plan.replacesListGroupingValue) {
if (plan.normalizedTargetGroupKey === null) {
delete frontmatter[plan.frontmatterKey];
} else {
frontmatter[plan.frontmatterKey] = [plan.normalizedTargetGroupKey];
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implements "move" semantics for drag & drop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant