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..ed65b598 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -143,9 +143,15 @@ 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'); + this.$addCommentBtnLink = this.$addCommentBtn.find('a'); + // On click comment icon toolbar - $('.addComment').on('click', (e) => { + this.$addCommentBtn.on('click', (e) => { e.preventDefault(); // stops focus from being lost + if (this.$addCommentBtn.hasClass('disabled')) return; this.displayNewCommentForm(); }); @@ -1036,6 +1042,20 @@ 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 = this.$addCommentBtn; + const $a = this.$addCommentBtnLink; + if (!$btn) return; + 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 +1315,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 +1366,13 @@ 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 ep = pad.plugins.ep_comments_page; + ep.updateAddCommentButtonState(!ep.checkNoTextSelected(rep)); + } } return; }, diff --git a/static/tests/frontend-new/helper/comments.ts b/static/tests/frontend-new/helper/comments.ts index 8d7122ac..8f9b8f70 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 () => + 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..4844e927 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,83 @@ 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}) => { + // aNewCommentsPad waits for the plugin to initialise, which can take up to 60s. + 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); + }); +});