diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index f6ed4f8..2abb197 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -1768,3 +1768,49 @@ test('MCP-adressen svarer JSON, ikke app-skallen', async ({ request }) => { expect(response.headers()['content-type']).toContain('application/json'); expect((await response.json()).code).toBe('unauthorized'); }); + +test('man kan skrive hvor som helst, og feltet fortæller hvad det forstår', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.goto('/'); + + // Nothing focused, nothing clicked: just type. The thought does not have to + // survive a journey to find somewhere to put it. + await page.locator('body').click({ position: { x: 5, y: 5 } }); + // "i dag" because this is the Today view: a task parsed onto no date at all is + // correctly absent from it, which would read as the capture having failed. + await page.keyboard.type('ring til Anders i dag'); + + const field = page.locator('[data-quickadd]'); + await expect(field).toBeFocused(); + // Every letter arrives, including the first — which is the one a naive + // implementation drops while it is busy moving focus. + await expect(field).toHaveValue('ring til Anders i dag'); + + // And the legend appears with the field, saying what the parser can read. It + // used to live in the placeholder, which vanishes at the first keystroke — + // exactly when the help starts being useful. + await expect(page.getByText('projekt', { exact: false }).first()).toBeVisible(); + + await page.keyboard.press('Enter'); + await expect(page.getByText('ring til Anders', { exact: true })).toBeVisible(); + + // In English too. The shortcut used to find the field by its Danish label, so + // it silently did nothing for anybody running the interface in English. + await page.goto('/indstillinger'); + await page.getByLabel('Sprog').selectOption('en'); + await page.getByRole('button', { name: 'Gem' }).first().click(); + await page.goto('/'); + await page.locator('body').click({ position: { x: 5, y: 5 } }); + await page.keyboard.type('call Anders'); + await expect(page.locator('[data-quickadd]')).toHaveValue('call Anders'); + + // Back to Danish. The account is shared with every test after this one, and + // leaving it in English made three of them hunt for buttons by names that no + // longer existed — a failure that looks like the feature under test and is not. + await page.goto('/indstillinger'); + await page.getByLabel('Language').selectOption('da'); + await page.getByRole('button', { name: 'Save' }).first().click(); + await expect(page.getByRole('button', { name: 'Gem' }).first()).toBeVisible(); + + expect(trouble).toEqual([]); +}); diff --git a/web/src/lib/components/QuickAdd.svelte b/web/src/lib/components/QuickAdd.svelte index 74e76b6..402bb81 100644 --- a/web/src/lib/components/QuickAdd.svelte +++ b/web/src/lib/components/QuickAdd.svelte @@ -21,6 +21,19 @@ let spans = $state([]); let submitting = $state(false); let input; + let focused = $state(false); + + // Kept beside the parser's own vocabulary on purpose: a legend that promises + // something internal/quickadd cannot read is worse than no legend. These four + // are what Parse recognises as marks; dates it reads as prose, which is why the + // last entry is an example rather than a symbol. + let syntax = $derived([ + { mark: '#', what: t('task.syntaxProject') }, + { mark: '/', what: t('task.syntaxSection') }, + { mark: '@', what: t('task.syntaxLabel') }, + { mark: 'p1', what: t('task.syntaxPriority') }, + { mark: t('task.syntaxDateMark'), what: t('task.syntaxDate') } + ]); let previewController = null; let previewTimer = null; @@ -143,7 +156,10 @@ bind:this={input} bind:value={text} {onkeydown} + onfocus={() => (focused = true)} + onblur={() => (focused = false)} type="text" + data-quickadd placeholder={t('task.placeholder')} aria-label={t('task.new')} autocomplete="off" @@ -154,10 +170,52 @@ {#if text.trim()} {/if} + + + {#if focused} +

+ {#each syntax as item} + {item.mark} {item.what} + {/each} +

+ {/if}