mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-08 15:47:40 +00:00
Reimplement editor Tab handling with accessibility safeguards (#6813)
Some checks are pending
/ release (push) Waiting to run
testing / test-e2e (push) Blocked by required conditions
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Some checks are pending
/ release (push) Waiting to run
testing / test-e2e (push) Blocked by required conditions
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
The primary goal is to balance having the editor work as expected by developers (with Tab key affecting indentation) while also not impeding keyboard navigation.
* Tab indents, Shift+Tab unindents, but only when that indent would be valid. E.g. moving existing list items down or up one level.
* Indenting a selection always works.
* When an "invalid" indent is attempted, nothing happens and a toast is shown with a hint to press again to leave the editor.
* Attempting the same action again allows the textarea lose focus by allowing the browser's default key handler.
* Pressing Esc also loses focus immediately.
* No tab handling happens until the text editor has been interacted with (other than just having been focused).
* Changing indentation in block quotes adds or removes quote levels instead.
Screenshot of the toast being shown:
a6287d29
-4ce0-4977-aae8-ef1aff2ac89f
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6813
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Danko Aleksejevs <danko@very.lv>
Co-committed-by: Danko Aleksejevs <danko@very.lv>
This commit is contained in:
parent
8b93f41aaa
commit
d483dc674a
8 changed files with 278 additions and 30 deletions
|
@ -39,7 +39,7 @@ test('Markdown image preview behaviour', async ({page}, workerInfo) => {
|
|||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('Markdown indentation', async ({page}) => {
|
||||
test('Markdown indentation via toolbar', async ({page}) => {
|
||||
const initText = `* first\n* second\n* third\n* last`;
|
||||
|
||||
const response = await page.goto('/user2/repo1/issues/new');
|
||||
|
@ -50,7 +50,6 @@ test('Markdown indentation', async ({page}) => {
|
|||
const indent = page.locator('button[data-md-action="indent"]');
|
||||
const unindent = page.locator('button[data-md-action="unindent"]');
|
||||
await textarea.fill(initText);
|
||||
await textarea.click(); // Tab handling is disabled until pointer event or input.
|
||||
|
||||
// Indent, then unindent first line
|
||||
await textarea.focus();
|
||||
|
@ -109,6 +108,146 @@ test('Markdown indentation', async ({page}) => {
|
|||
await expect(textarea).toHaveValue(initText);
|
||||
});
|
||||
|
||||
test('markdown indentation with Tab', async ({page}) => {
|
||||
const initText = `* first\n* second\n* third\n* last`;
|
||||
|
||||
const response = await page.goto('/user2/repo1/issues/new');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const textarea = page.locator('textarea[name=content]');
|
||||
const toast = page.locator('.toastify');
|
||||
const tab = ' ';
|
||||
|
||||
await textarea.fill(initText);
|
||||
|
||||
await textarea.click(); // Tab handling is disabled until pointer event or input.
|
||||
|
||||
// Indent, then unindent first line
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
|
||||
// Attempt unindent again, ensure focus is not immediately lost and toast is shown, but then focus is lost on next attempt.
|
||||
await expect(toast).toBeHidden(); // toast should not already be there
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toBeFocused();
|
||||
await expect(toast).toBeVisible();
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).not.toBeFocused();
|
||||
|
||||
// Indent lines 2-4
|
||||
await textarea.click();
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('\n') + 1, it.value.length));
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}* third\n${tab}* last`);
|
||||
|
||||
// Indent second line while in whitespace, then unindent.
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf(' * third'), it.value.indexOf(' * third')));
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}${tab}* third\n${tab}* last`);
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}* third\n${tab}* last`);
|
||||
|
||||
// Select all and unindent, then lose focus.
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.select());
|
||||
await textarea.press('Shift+Tab'); // Everything is unindented.
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
await textarea.press('Shift+Tab'); // Valid, but nothing happens -> switch to "about to lose focus" state.
|
||||
await expect(textarea).toBeFocused();
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).not.toBeFocused();
|
||||
|
||||
// Attempt the same with cursor within list element body.
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
|
||||
await textarea.press('ArrowRight');
|
||||
await textarea.press('ArrowRight');
|
||||
await textarea.press('Tab');
|
||||
// Whole line should be indented.
|
||||
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
|
||||
await textarea.press('Shift+Tab');
|
||||
|
||||
// Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text
|
||||
const line3 = `* first\n* second\n${tab}* third\n* last`;
|
||||
const lines23 = `* first\n${tab}* second\n${tab}${tab}* third\n* last`;
|
||||
await textarea.focus();
|
||||
await textarea.fill(line3);
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(lines23);
|
||||
await expect(textarea).toHaveJSProperty('selectionStart', lines23.indexOf('cond'));
|
||||
await expect(textarea).toHaveJSProperty('selectionEnd', lines23.indexOf('hird'));
|
||||
|
||||
// Then unindent twice, erasing all indents.
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toHaveValue(line3);
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
|
||||
// Check that partial indents are cleared
|
||||
await textarea.focus();
|
||||
await textarea.fill(initText);
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second')));
|
||||
await textarea.pressSequentially(' ');
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
});
|
||||
|
||||
test('markdown block quote indentation', async ({page}) => {
|
||||
const initText = `> first\n> second\n> third\n> last`;
|
||||
|
||||
const response = await page.goto('/user2/repo1/issues/new');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const textarea = page.locator('textarea[name=content]');
|
||||
const toast = page.locator('.toastify');
|
||||
|
||||
await textarea.fill(initText);
|
||||
|
||||
await textarea.click(); // Tab handling is disabled until pointer event or input.
|
||||
|
||||
// Indent, then unindent first line twice (quotes can quote quotes!)
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(`> > first\n> second\n> third\n> last`);
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(`> > > first\n> second\n> third\n> last`);
|
||||
await textarea.press('Shift+Tab');
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
|
||||
// Attempt unindent again.
|
||||
await expect(toast).toBeHidden(); // toast should not already be there
|
||||
await textarea.press('Shift+Tab');
|
||||
// Nothing happens - quote should not stop being a quote
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
// Focus is not immediately lost and toast is shown,
|
||||
await expect(textarea).toBeFocused();
|
||||
await expect(toast).toBeVisible();
|
||||
// Focus is lost on next attempt,
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).not.toBeFocused();
|
||||
|
||||
// Indent lines 2-4
|
||||
await textarea.click();
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('\n') + 1, it.value.length));
|
||||
await textarea.press('Tab');
|
||||
await expect(textarea).toHaveValue(`> first\n> > second\n> > third\n> > last`);
|
||||
|
||||
// Select all and unindent, then lose focus.
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.select());
|
||||
await textarea.press('Shift+Tab'); // Everything is unindented.
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
await textarea.press('Shift+Tab'); // Valid, but nothing happens -> switch to "about to lose focus" state.
|
||||
await expect(textarea).toBeFocused();
|
||||
await textarea.press('Shift+Tab');
|
||||
await expect(textarea).not.toBeFocused();
|
||||
});
|
||||
|
||||
test('Markdown list continuation', async ({page}) => {
|
||||
const initText = `* first\n* second`;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue