From be23a230f2214bc6725213126df478fbc4342988 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:14:40 +0000 Subject: [PATCH 1/6] Initial plan From 2d94593f10a7ec18e5d25d31d9ab47a965b44622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:38:30 +0000 Subject: [PATCH 2/6] feat: disable Add Comment toolbar button when no text is selected - Add `updateAddCommentButtonState(hasSelection)` to EpComments prototype - Call it in `postAceInit` (initial disabled state) and `aceEditEvent` (on every selection change) - Guard the `.addComment` click handler to bail out when disabled - Add `.addComment.disabled` CSS: opacity + cursor + pointer-events on child - Update `clickAddCommentButton` test helper to wait for enabled state - Add five new Playwright tests covering disabled/enabled button behaviour Agent-Logs-Url: https://github.com/ether/ep_comments_page/sessions/05bd6554-0537-4892-b608-28221070422f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --- static/css/comment.css | 9 ++ static/js/index.js | 25 ++++++ static/tests/frontend-new/helper/comments.ts | 6 ++ .../frontend-new/specs/newComment.spec.ts | 83 ++++++++++++++++++- 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/static/css/comment.css b/static/css/comment.css index 4c86f730..68226ee5 100644 --- a/static/css/comment.css +++ b/static/css/comment.css @@ -221,4 +221,13 @@ input.error, textarea.error { /* OTHER */ .hidden { display: none; +} + +/* Disabled state for the Add Comment toolbar button */ +.addComment.disabled { + opacity: 0.4; + cursor: not-allowed; +} +.addComment.disabled > a { + pointer-events: none; } \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js index 4996cc46..9dac39b6 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -146,6 +146,7 @@ EpComments.prototype.init = async function () { // On click comment icon toolbar $('.addComment').on('click', (e) => { e.preventDefault(); // stops focus from being lost + if ($('.addComment').hasClass('disabled')) return; this.displayNewCommentForm(); }); @@ -1036,6 +1037,19 @@ EpComments.prototype.checkNoTextSelected = function (rep) { return noTextSelected; }; +// Enable or disable the "Add Comment" toolbar button based on whether text is selected. +EpComments.prototype.updateAddCommentButtonState = function (hasSelection) { + const $btn = $('.addComment'); + const $a = $btn.find('a'); + if (hasSelection) { + $btn.removeClass('disabled'); + $a.removeAttr('aria-disabled'); + } else { + $btn.addClass('disabled'); + $a.attr('aria-disabled', 'true'); + } +}; + // Create form to add comment EpComments.prototype.createNewCommentFormIfDontExist = function (rep) { const data = this.getCommentData(); @@ -1295,6 +1309,9 @@ const hooks = { await Comments.initDone; pad.plugins.ep_comments_page = Comments; + // Start with the button disabled — no text is selected on load. + Comments.updateAddCommentButtonState(false); + if (!$('#editorcontainerbox').hasClass('flex-layout')) { $.gritter.add({ title: 'Error', @@ -1343,6 +1360,14 @@ const hooks = { pad.plugins.ep_comments_page.shouldCollectComment = false; }); } + + // Update toolbar button enabled/disabled state based on whether text is selected. + const rep = context.rep; + if (rep) { + const hasSelection = rep.selStart[0] !== rep.selEnd[0] || + rep.selStart[1] !== rep.selEnd[1]; + pad.plugins.ep_comments_page.updateAddCommentButtonState(hasSelection); + } } return; }, diff --git a/static/tests/frontend-new/helper/comments.ts b/static/tests/frontend-new/helper/comments.ts index 8d7122ac..c29d180f 100644 --- a/static/tests/frontend-new/helper/comments.ts +++ b/static/tests/frontend-new/helper/comments.ts @@ -91,7 +91,13 @@ export const selectLine = async (page: Page, lineIndex: number): Promise = }; // Clicks the toolbar Add Comment button (lives in the chrome page). +// Waits for the button to be enabled (i.e., text is currently selected in the editor). export const clickAddCommentButton = async (page: Page): Promise => { + await expect.poll(async () => + !(await page.locator('.addComment').first().evaluate( + (el: Element) => el.classList.contains('disabled') + )) + ).toBe(true); await page.locator('.addComment').first().click(); }; diff --git a/static/tests/frontend-new/specs/newComment.spec.ts b/static/tests/frontend-new/specs/newComment.spec.ts index d54ac09e..4e574892 100644 --- a/static/tests/frontend-new/specs/newComment.spec.ts +++ b/static/tests/frontend-new/specs/newComment.spec.ts @@ -1,6 +1,6 @@ import {expect, test} from '@playwright/test'; import {getPadBody} from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper'; -import {aNewCommentsPad} from '../helper/comments'; +import {aNewCommentsPad, clickAddCommentButton} from '../helper/comments'; test.describe('ep_comments_page - New comment', () => { test('new comment button focuses on comment textarea', async ({page}) => { @@ -23,3 +23,84 @@ test.describe('ep_comments_page - New comment', () => { expect(isFocused).toBe(true); }); }); + +test.describe('ep_comments_page - Add Comment button disabled state', () => { + test.beforeEach(async ({page}) => { + test.setTimeout(60_000); + }); + + test('button is disabled on load (no text selected)', async ({page}) => { + await aNewCommentsPad(page); + await expect.poll(async () => + page.locator('.addComment').first().evaluate( + (el: Element) => el.classList.contains('disabled') + ) + ).toBe(true); + const ariaDisabled = await page.locator('.addComment a').first() + .getAttribute('aria-disabled'); + expect(ariaDisabled).toBe('true'); + }); + + test('button is enabled when text is selected', async ({page}) => { + await aNewCommentsPad(page); + const inner = await getPadBody(page); + await inner.click(); + await page.keyboard.type('some text'); + await inner.locator('div').first().click({clickCount: 3}); + // Button should become enabled after selection. + await expect.poll(async () => + page.locator('.addComment').first().evaluate( + (el: Element) => el.classList.contains('disabled') + ) + ).toBe(false); + const ariaDisabled = await page.locator('.addComment a').first() + .getAttribute('aria-disabled'); + expect(ariaDisabled).toBeNull(); + }); + + test('button becomes disabled again after selection is cleared', async ({page}) => { + await aNewCommentsPad(page); + const inner = await getPadBody(page); + await inner.click(); + await page.keyboard.type('some text'); + // Select text. + await inner.locator('div').first().click({clickCount: 3}); + await expect.poll(async () => + page.locator('.addComment').first().evaluate( + (el: Element) => !el.classList.contains('disabled') + ) + ).toBe(true); + // Click elsewhere to deselect. + await inner.click(); + // Button should become disabled again. + await expect.poll(async () => + page.locator('.addComment').first().evaluate( + (el: Element) => el.classList.contains('disabled') + ) + ).toBe(true); + }); + + test('clicking disabled button does not open the comment form', async ({page}) => { + await aNewCommentsPad(page); + // Ensure no text is selected (button is disabled). + await expect.poll(async () => + page.locator('.addComment').first().evaluate( + (el: Element) => el.classList.contains('disabled') + ) + ).toBe(true); + // Click the
  • directly (bypassing pointer-events: none on the ). + await page.locator('.addComment').first().click(); + // The comment form must not appear. + await expect(page.locator('#newComment.popup-show')).toHaveCount(0); + }); + + test('clicking enabled button opens the comment form', async ({page}) => { + await aNewCommentsPad(page); + const inner = await getPadBody(page); + await inner.click(); + await page.keyboard.type('some text'); + await inner.locator('div').first().click({clickCount: 3}); + await clickAddCommentButton(page); + await expect(page.locator('#newComment.popup-show')).toHaveCount(1); + }); +}); From 676d346e52e2708d357ee97c887dc57031352d64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:40:06 +0000 Subject: [PATCH 3/6] perf: cache $addCommentBtn jQuery ref in init() to avoid repeated DOM lookups in aceEditEvent Agent-Logs-Url: https://github.com/ether/ep_comments_page/sessions/05bd6554-0537-4892-b608-28221070422f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --- static/js/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 9dac39b6..62401f91 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -143,10 +143,14 @@ EpComments.prototype.init = async function () { // `fromNow()` without needing to track the cached string anywhere. setInterval(() => this.refreshRelativeDates(), 60 * 1000); + // Cache the toolbar button element so it can be toggled efficiently in + // aceEditEvent (which fires on every keystroke / selection change). + this.$addCommentBtn = $('.addComment'); + // On click comment icon toolbar - $('.addComment').on('click', (e) => { + this.$addCommentBtn.on('click', (e) => { e.preventDefault(); // stops focus from being lost - if ($('.addComment').hasClass('disabled')) return; + if (this.$addCommentBtn.hasClass('disabled')) return; this.displayNewCommentForm(); }); @@ -1039,7 +1043,8 @@ EpComments.prototype.checkNoTextSelected = function (rep) { // Enable or disable the "Add Comment" toolbar button based on whether text is selected. EpComments.prototype.updateAddCommentButtonState = function (hasSelection) { - const $btn = $('.addComment'); + const $btn = this.$addCommentBtn; + if (!$btn) return; const $a = $btn.find('a'); if (hasSelection) { $btn.removeClass('disabled'); From 9ad8aaa1b72bf6fccf1bbc54050b4b74bfa0d250 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:41:28 +0000 Subject: [PATCH 4/6] refactor: cache $addCommentBtnLink and reuse checkNoTextSelected in aceEditEvent Agent-Logs-Url: https://github.com/ether/ep_comments_page/sessions/05bd6554-0537-4892-b608-28221070422f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --- static/js/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 62401f91..85f10976 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -146,6 +146,7 @@ EpComments.prototype.init = async function () { // Cache the toolbar button element so it can be toggled efficiently in // aceEditEvent (which fires on every keystroke / selection change). this.$addCommentBtn = $('.addComment'); + this.$addCommentBtnLink = this.$addCommentBtn.find('a'); // On click comment icon toolbar this.$addCommentBtn.on('click', (e) => { @@ -1044,8 +1045,8 @@ EpComments.prototype.checkNoTextSelected = function (rep) { // Enable or disable the "Add Comment" toolbar button based on whether text is selected. EpComments.prototype.updateAddCommentButtonState = function (hasSelection) { const $btn = this.$addCommentBtn; + const $a = this.$addCommentBtnLink; if (!$btn) return; - const $a = $btn.find('a'); if (hasSelection) { $btn.removeClass('disabled'); $a.removeAttr('aria-disabled'); @@ -1369,9 +1370,8 @@ const hooks = { // Update toolbar button enabled/disabled state based on whether text is selected. const rep = context.rep; if (rep) { - const hasSelection = rep.selStart[0] !== rep.selEnd[0] || - rep.selStart[1] !== rep.selEnd[1]; - pad.plugins.ep_comments_page.updateAddCommentButtonState(hasSelection); + pad.plugins.ep_comments_page.updateAddCommentButtonState( + !pad.plugins.ep_comments_page.checkNoTextSelected(rep)); } } return; From 1c3cce450f5e3eaa3d6489042d4ad9a287e99af7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:43:34 +0000 Subject: [PATCH 5/6] style: fix indentation and add comment on beforeEach timeout Agent-Logs-Url: https://github.com/ether/ep_comments_page/sessions/05bd6554-0537-4892-b608-28221070422f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --- static/js/index.js | 4 ++-- static/tests/frontend-new/specs/newComment.spec.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 85f10976..ed65b598 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1370,8 +1370,8 @@ const hooks = { // Update toolbar button enabled/disabled state based on whether text is selected. const rep = context.rep; if (rep) { - pad.plugins.ep_comments_page.updateAddCommentButtonState( - !pad.plugins.ep_comments_page.checkNoTextSelected(rep)); + const ep = pad.plugins.ep_comments_page; + ep.updateAddCommentButtonState(!ep.checkNoTextSelected(rep)); } } return; diff --git a/static/tests/frontend-new/specs/newComment.spec.ts b/static/tests/frontend-new/specs/newComment.spec.ts index 4e574892..4844e927 100644 --- a/static/tests/frontend-new/specs/newComment.spec.ts +++ b/static/tests/frontend-new/specs/newComment.spec.ts @@ -26,6 +26,7 @@ test.describe('ep_comments_page - New comment', () => { test.describe('ep_comments_page - Add Comment button disabled state', () => { test.beforeEach(async ({page}) => { + // aNewCommentsPad waits for the plugin to initialise, which can take up to 60s. test.setTimeout(60_000); }); @@ -36,8 +37,7 @@ test.describe('ep_comments_page - Add Comment button disabled state', () => { (el: Element) => el.classList.contains('disabled') ) ).toBe(true); - const ariaDisabled = await page.locator('.addComment a').first() - .getAttribute('aria-disabled'); + const ariaDisabled = await page.locator('.addComment a').first().getAttribute('aria-disabled'); expect(ariaDisabled).toBe('true'); }); @@ -53,8 +53,7 @@ test.describe('ep_comments_page - Add Comment button disabled state', () => { (el: Element) => el.classList.contains('disabled') ) ).toBe(false); - const ariaDisabled = await page.locator('.addComment a').first() - .getAttribute('aria-disabled'); + const ariaDisabled = await page.locator('.addComment a').first().getAttribute('aria-disabled'); expect(ariaDisabled).toBeNull(); }); From 6aaef454cd87b36a120ca49bec49c988adb3807b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:44:39 +0000 Subject: [PATCH 6/6] style: simplify double negation in clickAddCommentButton helper Agent-Logs-Url: https://github.com/ether/ep_comments_page/sessions/05bd6554-0537-4892-b608-28221070422f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --- static/tests/frontend-new/helper/comments.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/tests/frontend-new/helper/comments.ts b/static/tests/frontend-new/helper/comments.ts index c29d180f..8f9b8f70 100644 --- a/static/tests/frontend-new/helper/comments.ts +++ b/static/tests/frontend-new/helper/comments.ts @@ -94,9 +94,9 @@ export const selectLine = async (page: Page, lineIndex: number): Promise = // Waits for the button to be enabled (i.e., text is currently selected in the editor). export const clickAddCommentButton = async (page: Page): Promise => { await expect.poll(async () => - !(await page.locator('.addComment').first().evaluate( - (el: Element) => el.classList.contains('disabled') - )) + page.locator('.addComment').first().evaluate( + (el: Element) => !el.classList.contains('disabled') + ) ).toBe(true); await page.locator('.addComment').first().click(); };