Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3059 (2024-09-01 18:54:31)

fix: Improve Edit Task modal calendar button behaviour
# Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Description

- Usability fixes to the new calendar button in the Edit Task modal.
- Some refactoring to simplify the code.

Much of this was done whilst pairing with @ilandikov.

Credit to @joethei for the code for adding the icon to the Svelte button - and to @javalent for the original idea in [Discord](https://discord.com/channels/686053708261228577/840286264964022302/902949764209987654).

## Motivation and Context

- Fix accessibility issues of the original SVG-icon-based fake button
- Use a nicer icon on the button

## How has this been tested?

- Exploratory testing in the sample vault

## Screenshots (if appropriate)

<img width="613" alt="image" src="https://github.com/user-attachments/assets/31db0777-feeb-40cd-98fd-d4463cb9b09c">


## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Obsidian/TaskModal.ts b/src/Obsidian/TaskModal.ts
index 9eae51a9..f24ad420 100644
--- a/src/Obsidian/TaskModal.ts
+++ b/src/Obsidian/TaskModal.ts
@@ -33,6 +33,13 @@ export class TaskModal extends Modal implements IFlatpickrUser {
         this.task = task;
         this.allTasks = allTasks;
         this.onSubmit = (updatedTasks: Task[]) => {
+            if (this.activeFlatpickrInstance) {
+                // Ignore onSubmit if the date-picker is open:
+                // For some unknown reason, making the date-picker lunched from a Button
+                // caused the modal to be automatically submitted before the user had
+                // even seen the date picker.
+                return;
+            }
             updatedTasks.length && onSubmit(updatedTasks);
             this.close();
         };
diff --git a/src/Renderer/TaskLineRenderer.ts b/src/Renderer/TaskLineRenderer.ts
index 702e5812..844df950 100644
--- a/src/Renderer/TaskLineRenderer.ts
+++ b/src/Renderer/TaskLineRenderer.ts
@@ -219,7 +219,7 @@ export class TaskLineRenderer {
                     span.addEventListener('click', (ev: MouseEvent) => {
                         ev.preventDefault(); // suppress the default click behavior
                         ev.stopPropagation(); // suppress further event propagation
-                        promptForDate(li, task, componentDateField, defaultTaskSaver);
+                        promptForDate(span, task, componentDateField, defaultTaskSaver);
                     });
 
                     span.addEventListener('contextmenu', (ev: MouseEvent) => {
diff --git a/src/ui/DateEditor.svelte b/src/ui/DateEditor.svelte
index a049d74b..e573bb2d 100644
--- a/src/ui/DateEditor.svelte
+++ b/src/ui/DateEditor.svelte
@@ -2,6 +2,8 @@
     import flatpickr from 'flatpickr';
     import { createEventDispatcher } from 'svelte';
 
+    import { setIcon } from 'obsidian';
+
     import { doAutocomplete } from '../DateTime/DateAbbreviations';
     import { parseTypedDateForDisplayUsingFutureDate } from '../DateTime/DateTools';
     import { labelContentWithAccessKey } from './EditTaskHelpers';
@@ -19,6 +21,12 @@
 
     const dispatch = createEventDispatcher();
 
+    const iconCalendarDays = (node: HTMLElement) => {
+        // For a more general implementation, see:
+        // https://github.com/joethei/obsidian-rss/blob/b600e2ead2505d58aa3e4c7898795bbf58fa3cdc/src/view/IconComponent.svelte
+        setIcon(node, 'calendar-days');
+    };
+
     $: {
         date = doAutocomplete(date);
         parsedDate = parseTypedDateForDisplayUsingFutureDate(id, date, forwardOnly);
@@ -96,20 +104,13 @@
     {accesskey}
 />
 
-<!-- Separate the calendar icon from the input to allow typing in the input box -->
-<!-- TODO Fix the accessibility warning, ideally by converting this to a proper button -->
-<!-- TODO Nicer looking icon, or at least make it a paler shade -->
-<svg
-    class="tasks-modal-calendar-icon"
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 24 24"
+<button
+    class="tasks-modal-calendar-button"
+    use:iconCalendarDays
     on:click={openDatePicker}
-    style="cursor: pointer;"
->
-    <path
-        d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"
-    />
-</svg>
+    aria-label="Open date picker"
+    style="background: none; border: none; padding: 0; cursor: pointer;"
+/>
 
 <code class="tasks-modal-parsed-date">{dateSymbol} {@html parsedDate}</code>
 
diff --git a/src/ui/EditTask.scss b/src/ui/EditTask.scss
index e5e75951..0c1d8f99 100644
--- a/src/ui/EditTask.scss
+++ b/src/ui/EditTask.scss
@@ -81,11 +81,9 @@
         min-width: 15em;
     }
 
-    .tasks-modal-calendar-icon {
+    .tasks-modal-calendar-button {
         grid-column: 3;
-        width: 24px;
-        height: 24px;
-        fill: var(--text-muted);
+        color: var(--text-muted);
     }
 
     .tasks-modal-parsed-date {
@@ -164,7 +162,7 @@
             grid-column: 1;
         }
 
-        .tasks-modal-calendar-icon {
+        .tasks-modal-calendar-button {
             grid-column: 2;
         }
 
@@ -194,7 +192,7 @@
             grid-column: 1;
         }
 
-        .tasks-modal-calendar-icon {
+        .tasks-modal-calendar-button {
             grid-column: 1;
         }
 
diff --git a/src/ui/Menus/DatePicker.ts b/src/ui/Menus/DatePicker.ts
index dd383339..9919b3d8 100644
--- a/src/ui/Menus/DatePicker.ts
+++ b/src/ui/Menus/DatePicker.ts
@@ -19,42 +19,26 @@ export function promptForDate(
     dateFieldToEdit: AllTaskDateFields,
     taskSaver: (originalTask: Task, newTasks: Task | Task[]) => Promise<void>,
 ) {
-    if (!parentElement) {
-        console.log('Parent element not found.');
-        return;
-    }
+    const currentValue = task[dateFieldToEdit];
+    // TODO figure out how Today's date is determined: if Obsidian is left
+    //      running overnight, the flatpickr modal shows the previous day as Today.
+    const fp = flatpickr(parentElement, {
+        defaultDate: currentValue ? currentValue.format('YYYY-MM-DD') : new Date(),
+        enableTime: false, // Optional: Enable time picker
+        dateFormat: 'Y-m-d', // Adjust the date and time format as needed
+        locale: {
+            firstDayOfWeek: 1, // Sets Monday as the first day of the week
+        },
+        onClose: async (selectedDates, _dateStr, instance) => {
+            if (selectedDates.length > 0) {
+                const date = selectedDates[0];
+                const newTask = new SetTaskDate(dateFieldToEdit, date).apply(task);
+                await taskSaver(task, newTask);
+            }
+            instance.destroy(); // Proper cleanup
+        },
+    });
 
-    const input = document.createElement('input');
-    input.type = 'text'; // Flatpickr can hook into a text input
-    parentElement.appendChild(input);
-
-    // Ensure styles are applied so Flatpickr can render correctly
-    input.style.minWidth = '200px'; // Ensure there's enough room for Flatpickr
-
-    // Delay the initialization of Flatpickr to ensure DOM is ready
-    setTimeout(() => {
-        const currentValue = task[dateFieldToEdit];
-        // TODO figure out how Today's date is determined: if Obsidian is left
-        //      running overnight, the flatpickr modal shows the previous day as Today.
-        const fp = flatpickr(input, {
-            defaultDate: currentValue ? currentValue.format('YYYY-MM-DD') : new Date(),
-            enableTime: false, // Optional: Enable time picker
-            dateFormat: 'Y-m-d', // Adjust the date and time format as needed
-            locale: {
-                firstDayOfWeek: 1, // Sets Monday as the first day of the week
-            },
-            onClose: async (selectedDates, _dateStr, instance) => {
-                if (selectedDates.length > 0) {
-                    const date = selectedDates[0];
-                    const newTask = new SetTaskDate(dateFieldToEdit, date).apply(task);
-                    await taskSaver(task, newTask);
-                }
-                instance.destroy(); // Proper cleanup
-                input.remove(); // Remove the elements after selection
-            },
-        });
-
-        // Open the calendar programmatically
-        fp.open();
-    }, 0);
+    // Open the calendar programmatically
+    fp.open();
 }


Test patch:
diff --git a/tests/__mocks__/obsidian.ts b/tests/__mocks__/obsidian.ts
index c59ffa9b..ea0dbc31 100644
--- a/tests/__mocks__/obsidian.ts
+++ b/tests/__mocks__/obsidian.ts
@@ -157,3 +157,6 @@ export function prepareSimpleSearch(query: string): (text: string) => SearchResu
         return caseInsensitiveSubstringSearch(query, text);
     };
 }
+
+type IconName = string;
+export function setIcon(_parent: HTMLElement, _iconId: IconName): void {}
diff --git a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html
index dc0df6cd..d5e95626 100644
--- a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html
+++ b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html
@@ -94,14 +94,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="d" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         📅
         <i>no due date</i>
@@ -116,14 +112,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="s" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ⏳
         <i>no scheduled date</i>
@@ -139,14 +131,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="a" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         🛫
         <i>no start date</i>
@@ -245,14 +233,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="c" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ➕
         <i>no created date</i>
@@ -268,14 +252,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="x" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ✅
         <i>no done date</i>
@@ -291,14 +271,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="-" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ❌
         <i>no cancelled date</i>
diff --git a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html
index bc199a2e..8c67004b 100644
--- a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html
+++ b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html
@@ -56,42 +56,30 @@
       </code>
       <label for="due">Due</label>
       <input id="due" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         📅
         <i>no due date</i>
       </code>
       <label for="scheduled">Scheduled</label>
       <input id="scheduled" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ⏳
         <i>no scheduled date</i>
       </code>
       <label for="start">Start</label>
       <input id="start" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         🛫
         <i>no start date</i>
@@ -165,42 +153,30 @@
       </select>
       <label for="created">Created</label>
       <input id="created" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ➕
         <i>no created date</i>
       </code>
       <label for="done">Done</label>
       <input id="done" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ✅
         <i>no done date</i>
       </code>
       <label for="cancelled">Cancelled</label>
       <input id="cancelled" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <svg
-        class="tasks-modal-calendar-icon"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 24 24"
-        style="cursor: pointer">
-        <path
-          d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM7 11h5v5H7z"></path>
-      </svg>
+      <button
+        class="tasks-modal-calendar-button"
+        aria-label="Open date picker"
+        style="background: none; padding: 0px; cursor: pointer"></button>
       <code class="tasks-modal-parsed-date">
         ❌
         <i>no cancelled date</i>