Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3169 (2024-11-04 15:35:12)

feat: Add date-picker to the Edit Task modal
# Types of changes

Done by pairing with @claremacrae 

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
  - Issue/discussion:
    - #2649
    - #2190 

## Description

- add date picker for the dates in the Edit Task modal
- fix helper function testing that an element has been rendered (`getAndCheckRenderedElement()`)

## Motivation and Context

- Quicker way to select dates in the Edit Task modal

## How has this been tested?

- Unit tests
- Exploratory testing 

## Screenshots (if appropriate)


https://github.com/user-attachments/assets/94921f53-5e3f-49b9-aa94-9926402277b6



## Checklist

- [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

- [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/ui/DateEditor.svelte b/src/ui/DateEditor.svelte
index a3c92e86..12c2f9ec 100644
--- a/src/ui/DateEditor.svelte
+++ b/src/ui/DateEditor.svelte
@@ -23,10 +23,22 @@
     //     setIcon(node, 'calendar-days');
     // };
 
+    let pickedDate = '';
+
     $: {
         date = doAutocomplete(date);
         parsedDate = parseTypedDateForDisplayUsingFutureDate(id, date, forwardOnly);
         isDateValid = !parsedDate.includes('invalid');
+        if (isDateValid) {
+            pickedDate = parsedDate;
+        }
+    }
+
+    function onDatePicked(e: Event) {
+        if (e.target === null) {
+            return;
+        }
+        date = pickedDate;
     }
 
     // 'weekend' abbreviation omitted due to lack of space.
@@ -106,8 +118,20 @@
 <!--    aria-label="Open date picker"-->
 <!--    style="background: none; border: none; padding: 0; cursor: pointer;"-->
 <!--/>-->
-
-<code class="tasks-modal-parsed-date">{dateSymbol} {@html parsedDate}</code>
+{#if isDateValid}
+    <div class="tasks-modal-parsed-date">
+        {dateSymbol}<input
+            class="tasks-modal-date-editor-picker"
+            type="date"
+            bind:value={pickedDate}
+            id="date-editor-picker"
+            on:input={onDatePicked}
+            tabindex="-1"
+        />
+    </div>
+{:else}
+    <code class="tasks-modal-parsed-date">{dateSymbol} {@html parsedDate}</code>
+{/if}
 
 <style>
 </style>
diff --git a/src/ui/EditTask.scss b/src/ui/EditTask.scss
index 53b5da95..be17e62e 100644
--- a/src/ui/EditTask.scss
+++ b/src/ui/EditTask.scss
@@ -81,6 +81,10 @@
         min-width: 15em;
     }
 
+    .tasks-modal-date-editor-picker {
+        margin-left: .5em;
+    }
+
     //.tasks-modal-calendar-button {
     //    grid-column: 3;
     //    color: var(--text-muted);


Test patch:
diff --git a/tests/ui/DateEditor.test.ts b/tests/ui/DateEditor.test.ts
index f6867911..0497b459 100644
--- a/tests/ui/DateEditor.test.ts
+++ b/tests/ui/DateEditor.test.ts
@@ -22,17 +22,24 @@ function testInputValue(container: HTMLElement, inputId: string, expectedText: s
     expect(input.value).toEqual(expectedText);
 }
 
+function testDatePickerValue(container: HTMLElement, expectedValue: string) {
+    const datePicker = getAndCheckRenderedElement<HTMLInputElement>(container, 'date-editor-picker');
+    expect(datePicker.value).toEqual(expectedValue);
+}
+
 async function testTypingInput(
     {
         userTyped,
         expectedLeftText,
         expectedRightText,
         expectedReturnedDate,
+        expectedReturnedDateValidity = 'true',
     }: {
         userTyped: string;
         expectedLeftText: string;
         expectedRightText: string;
         expectedReturnedDate: string;
+        expectedReturnedDateValidity?: 'true' | 'false';
     },
     { forwardOnly }: { forwardOnly: boolean } = { forwardOnly: true },
 ) {
@@ -44,6 +51,14 @@ async function testTypingInput(
     testInputValue(container, 'due', expectedLeftText);
     testInputValue(container, 'parsedDateFromDateEditor', expectedRightText);
     testInputValue(container, 'dueDateFromDateEditor', expectedReturnedDate);
+    testInputValue(container, 'parsedDateValidFromDateEditor', expectedReturnedDateValidity);
+
+    if (expectedReturnedDateValidity === 'true') {
+        testDatePickerValue(container, expectedRightText);
+    } else {
+        const datePicker = container.ownerDocument.getElementById('date-editor-picker') as HTMLInputElement;
+        expect(datePicker).toBeNull();
+    }
 }
 
 beforeEach(() => {
@@ -62,6 +77,8 @@ describe('date editor wrapper tests', () => {
         testInputValue(container, 'due', '');
         testInputValue(container, 'parsedDateFromDateEditor', '<i>no due date</i>');
         testInputValue(container, 'dueDateFromDateEditor', '');
+
+        testDatePickerValue(container, '');
     });
 
     it('should replace an empty date field with typed date value', async () => {
@@ -88,6 +105,7 @@ describe('date editor wrapper tests', () => {
             expectedLeftText: 'blah',
             expectedRightText: '<i>invalid due date</i>',
             expectedReturnedDate: 'blah',
+            expectedReturnedDateValidity: 'false',
         });
     });
 
@@ -114,4 +132,18 @@ describe('date editor wrapper tests', () => {
             { forwardOnly: false },
         );
     });
+
+    it('should pick a date', async () => {
+        const container = renderDateEditorWrapper({ forwardOnly: false });
+        const datePicker = getAndCheckRenderedElement<HTMLInputElement>(container, 'date-editor-picker');
+
+        await fireEvent.input(datePicker, { target: { value: '2024-11-03' } });
+
+        expect(datePicker.value).toEqual('2024-11-03');
+
+        testInputValue(container, 'due', '2024-11-03');
+        testInputValue(container, 'parsedDateFromDateEditor', '2024-11-03');
+        testInputValue(container, 'dueDateFromDateEditor', '2024-11-03');
+        testInputValue(container, 'parsedDateValidFromDateEditor', 'true');
+    });
 });
diff --git a/tests/ui/DateEditorWrapper.svelte b/tests/ui/DateEditorWrapper.svelte
index 026caa07..337d333d 100644
--- a/tests/ui/DateEditorWrapper.svelte
+++ b/tests/ui/DateEditorWrapper.svelte
@@ -22,6 +22,7 @@
 />
 <input bind:value={dateFromDateEditor} id="dueDateFromDateEditor" />
 <input bind:value={parsedDateFromDateEditor} id="parsedDateFromDateEditor" />
+<input bind:value={isDueDateValid} id="parsedDateValidFromDateEditor" />
 
 <style>
 </style>
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 7333f8b2..5891f9f1 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,10 +94,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="d" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         📅
-        <i>no due date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <label for="scheduled">
         <span class="accesskey">S</span>
         cheduled
@@ -105,12 +105,12 @@
       <input
         id="scheduled"
         type="text"
-        class="tasks-modal-date-input"
+        class="tasks-modal-date-input tasks-modal-error"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="s" />
       <code class="tasks-modal-parsed-date">
         ⏳
-        <i>no scheduled date</i>
+        <i>invalid scheduled date</i>
       </code>
       <label for="start">
         St
@@ -123,10 +123,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="a" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         🛫
-        <i>no start date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <div class="future-dates-only">
         <label for="forwardOnly">
           Only
@@ -221,10 +221,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="c" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         ➕
-        <i>no created date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <label for="done">
         Done (
         <span class="accesskey">x</span>
@@ -236,10 +236,10 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="x" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         ✅
-        <i>no done date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <label for="cancelled">
         Cancelled (
         <span class="accesskey">-</span>
@@ -251,13 +251,13 @@
         class="tasks-modal-date-input"
         placeholder="Try 'Mon' or 'tm' then space"
         accesskey="-" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         ❌
-        <i>no cancelled date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
     </section>
     <section class="tasks-modal-button-section">
-      <button type="submit" class="mod-cta">Apply</button>
+      <button type="submit" class="mod-cta" disabled="">Apply</button>
       <button type="button">Cancel</button>
     </section>
   </form>
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 e35d282d..5c8b4a67 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,22 +56,26 @@
       </code>
       <label for="due">Due</label>
       <input id="due" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         📅
-        <i>no due date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <label for="scheduled">Scheduled</label>
-      <input id="scheduled" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
+      <input
+        id="scheduled"
+        type="text"
+        class="tasks-modal-date-input tasks-modal-error"
+        placeholder="Try 'Mon' or 'tm' then space" />
       <code class="tasks-modal-parsed-date">
         ⏳
-        <i>no scheduled date</i>
+        <i>invalid 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" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         🛫
-        <i>no start date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <div class="future-dates-only">
         <label for="forwardOnly">Only future dates:</label>
         <input id="forwardOnly" type="checkbox" class="task-list-item-checkbox tasks-modal-checkbox" />
@@ -141,25 +145,25 @@
       </select>
       <label for="created">Created</label>
       <input id="created" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         ➕
-        <i>no created date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <label for="done">Done</label>
       <input id="done" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         ✅
-        <i>no done date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
       <label for="cancelled">Cancelled</label>
       <input id="cancelled" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
-      <code class="tasks-modal-parsed-date">
+      <div class="tasks-modal-parsed-date">
         ❌
-        <i>no cancelled date</i>
-      </code>
+        <input class="tasks-modal-date-editor-picker" type="date" id="date-editor-picker" tabindex="-1" />
+      </div>
     </section>
     <section class="tasks-modal-button-section">
-      <button type="submit" class="mod-cta">Apply</button>
+      <button type="submit" class="mod-cta" disabled="">Apply</button>
       <button type="button">Cancel</button>
     </section>
   </form>
diff --git a/tests/ui/EditTask.test.ts b/tests/ui/EditTask.test.ts
index 47a83898..1e142f99 100644
--- a/tests/ui/EditTask.test.ts
+++ b/tests/ui/EditTask.test.ts
@@ -658,7 +658,9 @@ describe('Edit Modal HTML snapshot tests', () => {
     });
 
     function verifyModalHTML() {
-        const task = taskFromLine({ line: '- [ ] absolutely to do', path: '' });
+        // Populate task a valid and an invalid date. Note that the valid date value
+        // is not visible in the HTML output.
+        const task = taskFromLine({ line: '- [ ] absolutely to do 🛫 2024-01-01 ⏳ 2024-02-33', path: '' });
         const onSubmit = () => {};
         const allTasks = [task];
         const { container } = renderAndCheckModal(task, onSubmit, allTasks);
diff --git a/tests/ui/RenderingTestHelpers.ts b/tests/ui/RenderingTestHelpers.ts
index 1da76842..04832340 100644
--- a/tests/ui/RenderingTestHelpers.ts
+++ b/tests/ui/RenderingTestHelpers.ts
@@ -9,7 +9,7 @@ import type EditTask from '../../src/ui/EditTask.svelte';
  */
 export function getAndCheckRenderedElement<T>(container: HTMLElement, elementId: string) {
     const element = container.ownerDocument.getElementById(elementId) as T;
-    expect(() => element).toBeTruthy();
+    expect(element).not.toBeNull();
     return element;
 }