fix(chat): icon click, disabled toggles, username layout (#7590, #7592, #7593) (#7597)

* fix(userlist): stop username input from overlapping the Log out button

Fixes #7593. In the pad's Users popup, #myusernameform had no width
set and the <input id="myusernameedit"> inside it took its natural
content width, pushing past the Log out button and making the button
overflow the popup at common widths.

Constrain #myusernameform to 75px and make the input fill its
container with box-sizing: border-box so the text field stays inside
the form and the Log out button sits visibly next to it rather than
getting covered or clipped off-screen.

Low-risk, CSS-only change. No test plan beyond visual verification
because the affected control is in the users popup UI.

* fix(chat): bottom-align titlebar controls; restore chat icon click (#7590)

Two regressions from the #7584 a11y refactor of the chat widget,
both pure-CSS fixes scoped to the chat panel.

1. Title bar — `<a>` → `<button>` for #titlecross/#titlesticky kept the
   `float: right` layout, but a `<button>`'s box is only as tall as its
   glyph, so the small `−` and `█` controls floated at the *top* of the
   44px title bar instead of sitting on the title's baseline as the
   anchors did. Switch #titlebar to a flex row with `align-items:
   flex-end`, give #titlelabel `flex: 1` to push the controls to the
   right edge, and use `order: 1/2` to keep the historical visual order
   `[█] [−]` (which `float: right` previously produced from reverse
   source order).

2. Chat-icon corner widget — `<div>` → `<button id="chaticon">` exposes
   the inner `<span class="buttonicon">` to the global `.buttonicon`
   rule's `display: flex; position: relative; align-items/justify-content:
   center;`. The existing override only reset `display`, leaving the
   span as a positioned flex item that, in some layouts, sat over the
   button's hit surface and swallowed clicks. Reset the remaining flex
   properties and add `pointer-events: none` so clicks always reach the
   `<button>`'s own click handler — preferred over weakening the global
   .buttonicon rule, which the toolbar relies on for icon centring.

Visual-only / behaviour-fix, no markup or JS changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): grey disabled chat option labels (#7592)

When "Disable chat" is ticked in the Settings dialog, refreshMyViewControls()
already sets `disabled` on `#options-stickychat` and `#options-chatandusers`,
but the browser only greys the checkbox itself — the adjacent `<label>`
keeps its normal colour, so the row still looks interactive even though
clicks are no-ops.

Add a popup-scoped rule that follows the existing convention used for
disabled `.nice-select` controls (`color: #999; cursor: not-allowed`) so
any disabled checkbox or radio in a settings popup matches its label to
the disabled state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(userlist): drop username input width cap (#7593 review)

The width:75px on #myusernameform and width:100%/box-sizing on
#myusernameedit from a55436ca0 were guarding against an overlap with
a "Log out" button — but no Log out button exists in vanilla
etherpad-lite (the original report came from a setup with a plugin
that adds one). Without that button visible, the cap just makes the
default username field unnecessarily narrow.

Restore #myusernameform to just `margin-left: 10px` and drop the
forced width on the input. If the overlap reappears in a real plugin
setup it should be re-fixed there (or with a more targeted rule that
only kicks in when a logout button is actually present).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(chat): keep titlesticky at top of title bar (#7590 review)

The previous pass bottom-aligned both corner controls via
align-items: flex-end on #titlebar. That correctly placed the close
button (#titlecross) on the title's baseline, but it also dragged the
much smaller "stick to screen" button (#titlesticky) down to the same
baseline — visibly far below where it sat in the original layout.

Switch to per-control align-self so each lands where it should:
  - #titlesticky → align-self: flex-start  (top, where it always was)
  - #titlecross  → align-self: flex-end    (bottom, on the title's baseline)
  - #titlelabel  → align-self: center      (don't stretch the heading)

Drop align-items from #titlebar so the defaults don't override these.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(chat): restore original #titlebar layout (#7590 review)

Both attempted CSS layouts for the title bar (full flex with
align-items: flex-end, then per-control align-self) ended up looking
worse than the original in review. Drop all the #titlebar / #titlelabel
/ #titlecross / #titlesticky changes from 905294d5b and f37da9a62 and
restore the pre-existing float-based layout. The chat panel ships with
its original visuals; we'll revisit #7590 separately if needed.

Keeps the chat-icon click fix from 905294d5b (#chaticon .buttonicon
flex/pointer-events reset) and the focus-visible additions for the
title-bar buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(chat): clear inline display:none in chat.show()

When the user disables chat in settings, applyShowChat(false) calls
\`$('#chatbox').hide()\` which sets the chatbox's inline display to
\`none\`. Re-enabling chat doesn't undo that — it only re-shows the
icon. Then clicking the icon runs chat.show(), which adds the
\`.visible\` class but only flips visibility, not display, so the
chatbox stays hidden by the lingering inline style and the chat
appears not to open.

Clear the inline display in chat.show() before adding the .visible
class so the box becomes visible regardless of how it got hidden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(colibris): align username gap; grey unchecked-disabled toggles

users.css: change #myusernameform margin-left from 35px to 10px to
match the base popup_users.css. The 35px value was chosen for the
sticky chatAndUsers layout, but for the standalone Users popup it
opens an unnecessarily wide gap between the colour swatch and the
username field. (#7593 review)

form.css: drop the \`:checked\` qualifier from the disabled toggle
visual rule so unchecked-but-disabled toggles also dim. Without this,
"Chat always on screen" / "Show Chat and Users" stayed fully bright
when "Disable chat" was ticked even though the underlying inputs were
disabled. Fixes #7592 in the colibris skin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(chat): simple flex titlebar — CHAT      _  []

Single flex row, vertically centred via align-items: center. Title
takes the remaining width with flex: 1; the two corner controls fall
in at the right edge in source order (titlecross then titlesticky),
giving the intended visual: minus on the left, sticky on the right.

Drops `float: right` from the controls, `display: inline` from the
heading, and the prior `padding-top: 2px` hack on titlesticky (flex
alignment handles the vertical position now).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(chat): titlebar uses underscore for minimize; symmetric padding

- Replace \`&minus;\` with \`_\` in #titlecross. The minus glyph sits at
  the centre of its em-box and read as a hyphen mid-row when the row
  was vertically centred; \`_\` sits at the bottom of its em-box and
  reads as a proper minimize indicator.
- Even out #titlebar horizontal padding to 9px and drop the asymmetric
  \`margin-left: 4px\` on #titlelabel so CHAT on the left and the
  sticky button on the right are the same distance from the bar's
  edges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(chat): lift #titlecross underscore 5px

The \`_\` glyph renders at the bottom of its em-box, so even with the
title bar's flex \`align-items: center\` it sits noticeably below the
CHAT baseline. Lift it with \`transform: translateY(-5px)\` (doesn't
affect flex layout calculations) so the underscore reads at roughly
the same vertical line as the title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(chat): cover #7590 / #7592 / #7593 fixes

Adds Playwright frontend specs for the changes in this PR:

chat.spec.ts
  - chat icon click reveals chatbox after disable→enable cycle
    (regression: chat.show() must clear inline display:none)
  - title bar lays out as a centred flex row with underscore minimize
    (covers display, align-items, label flex:1, no float, translateY
    lift, and visual padding symmetry via rendered geometry)
  - chat icon click reliably opens the chat box (#chaticon .buttonicon
    pointer/flex reset)

pad_settings.spec.ts
  - disabling chat disables and visually greys the dependent chat
    toggles (#7592 — checks input :disabled state and label opacity)

change_user_name.spec.ts
  - #myusernameform has 10px left margin and is not width-capped
    (#7593 review — colibris margin alignment, no input width cap)

Padding symmetry asserted via rendered rect deltas rather than the
CSS literal, since colibris ships its own #titlebar padding override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-04-26 16:56:38 +08:00 committed by GitHub
parent 83a42afbae
commit cd793294c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 192 additions and 15 deletions

View File

@ -59,20 +59,29 @@
}
/* -- TITLE BAR -- */
/* Single flex row, vertically centred: [ CHAT _ [] ]
- #titlelabel takes the remaining width so the controls sit at the
right edge.
- Source order is titlecross then titlesticky, which is also the
desired visual order (minus on the left, sticky on the right). */
#titlebar {
font-weight: bold;
padding: 5px;
/* Equal horizontal padding so CHAT on the left and the sticky button on
the right sit the same distance from the title-bar edges. */
padding: 5px 9px;
display: flex;
align-items: center;
gap: 8px;
}
#titlebar #titlelabel {
margin: 4px 0 0 4px;
display: inline;
margin: 0;
font-size: 1.4rem;
flex: 1;
}
#titlebar .stick-to-screen-btn,
#titlebar .hide-reduce-btn {
font-size: 25px;
color: inherit;
float: right;
text-align: right;
text-decoration: none;
cursor: pointer;
@ -84,7 +93,12 @@
}
#titlebar .stick-to-screen-btn {
font-size: 10px;
padding-top: 2px;
}
/* The `_` glyph in #titlecross renders at the bottom of its em-box, which
places it visibly far below the CHAT baseline. Lift it without changing
the flex row layout. */
#titlebar .hide-reduce-btn {
transform: translateY(-5px);
}
#titlebar .stick-to-screen-btn:focus-visible,
#titlebar .hide-reduce-btn:focus-visible {
@ -147,12 +161,22 @@
color: inherit;
}
/* Scope: the inner .buttonicon span here is just a glyph holder. Its global
rule in icons.css applies `display: flex` which is fine for toolbar
<button class="buttonicon"> instances but breaks inline layout when
nested inside a button that's laying out label + glyph + counter on one
line. Keep the glyph inline for the chat-icon corner widget. */
rule in icons.css applies `display: flex; align-items: center;
justify-content: center; position: relative;` which is fine for toolbar
`<button class="buttonicon">` instances (where the class IS the button)
but breaks the chat-icon corner widget here `.buttonicon` is on a
`<span>` *inside* the button, alongside two more inline `<span>`s for
the label and unread counter. Turning the middle span into a flex
container disrupts the inline row and, in some layouts, leaves it
sitting on top of the button's clickable surface so clicks never reach
the `<button>`'s own handler. Reset the flex behaviour and make the
span pointer-transparent so clicks always fall through to #chaticon. */
#chaticon .buttonicon {
display: inline;
align-items: initial;
justify-content: initial;
position: static;
pointer-events: none;
}
#chaticon:focus-visible {
outline: 2px solid #0066cc;

View File

@ -53,6 +53,17 @@
margin: 5px 0;
}
/* When a settings checkbox is disabled (e.g. "Chat always on screen" while
"Disable chat" is ticked), the browser greys the checkbox itself but the
adjacent `<label>` still looks active, leaving the row visually clickable
even though it isn't. Match the row to the checkbox's disabled state.
Fixes #7592. */
.popup input[type="checkbox"]:disabled + label,
.popup input[type="radio"]:disabled + label {
color: #999;
cursor: not-allowed;
}
/* Mobile devices */
@media only screen and (max-width: 800px) {
.popup {

View File

@ -36,7 +36,11 @@ exports.chat = (() => {
show() {
if (pad.settings.hideChat) return;
$('#chaticon').removeClass('visible');
$('#chatbox').addClass('visible');
// Clear any inline `display: none` left by applyShowChat(false)'s
// jQuery .hide() — without this, re-enabling chat then clicking the
// icon would only add the .visible class while the box stayed hidden
// by the inline style. The .visible class only flips visibility.
$('#chatbox').css('display', '').addClass('visible');
this.scrollDown(true);
chatMentions = 0;
Tinycon.setBubble(0);

View File

@ -109,9 +109,12 @@ select:hover, .nice-select:hover {
transform: translateX(14px);
}
[type="checkbox"]:checked:disabled + label,
[type="checkbox"]:checked:disabled + label:before,
[type="checkbox"]:checked:disabled + label:after {
/* Apply to any disabled checkbox (checked or unchecked), not just checked
ones, so dependent toggles like "Chat always on screen" visibly grey out
when "Disable chat" is ticked. Fixes #7592. */
[type="checkbox"]:disabled + label,
[type="checkbox"]:disabled + label:before,
[type="checkbox"]:disabled + label:after {
cursor: not-allowed;
opacity: .4;
}

View File

@ -26,7 +26,7 @@ table#otheruserstable {
}
#myusernameform {
margin-left: 35px;
margin-left: 10px;
}
input#myusernameedit {

View File

@ -455,7 +455,7 @@
<div class="chat-content">
<div id="titlebar">
<h1 id ="titlelabel" data-l10n-id="pad.chat"></h1>
<button type="button" id="titlecross" class="hide-reduce-btn" aria-label="Close chat">&minus;</button>
<button type="button" id="titlecross" class="hide-reduce-btn" aria-label="Close chat">_</button>
<button type="button" id="titlesticky" class="stick-to-screen-btn" data-l10n-id="pad.chat.stick.title">&#9608;</button>
</div>
<div id="chattext" class="thin-scrollbar" aria-live="polite" aria-relevant="additions removals text" role="log" aria-atomic="false">

View File

@ -33,3 +33,28 @@ test('Own user name is shown when you enter a chat', async ({page})=> {
expect(chatText).toContain('😃')
expect(chatText).toContain(chatMessage)
});
// #7593 review: the previous fix capped #myusernameform at 75px so a plugin-
// supplied "Log out" button wouldn't overflow, but vanilla etherpad-lite has
// no such button and the cap just made the username field too small. The
// colibris skin also pre-existing override of margin-left:35px (chosen for
// the chatAndUsers sticky layout) has been aligned with the base 10px.
test('#myusernameform has 10px left margin and is not width-capped', async ({page}) => {
await toggleUserList(page);
const styles = await page.evaluate(() => {
const form = document.querySelector('#myusernameform') as HTMLElement;
const input = document.querySelector('#myusernameedit') as HTMLElement;
return {
formMarginLeft: getComputedStyle(form).marginLeft,
formWidth: getComputedStyle(form).width,
inputWidth: getComputedStyle(input).width,
};
});
expect(styles.formMarginLeft).toBe('10px');
// The form should size to its content / parent flex behaviour, NOT be capped
// at 75px — width should comfortably exceed that.
expect(parseFloat(styles.formWidth)).toBeGreaterThan(80);
expect(parseFloat(styles.inputWidth)).toBeGreaterThan(80);
});

View File

@ -115,3 +115,79 @@ test('Checks showChat=false URL Parameter hides chat then' +
// chat should be visible.
expect(await secondChatIcon.isVisible()).toBe(true)
});
// Regression: applyShowChat(false) sets inline `display: none` on #chatbox via
// jQuery .hide(); re-enabling chat doesn't undo it, and chat.show() only flips
// visibility via the .visible class — so without an explicit display reset the
// box stays hidden by the lingering inline style. (PR #7597)
test('chat icon click reveals chatbox after a disable → enable cycle', async ({page}) => {
await showSettings(page);
await page.locator('label[for="options-disablechat"]').click();
await expect(page.locator('#options-disablechat')).toBeChecked();
await expect(page.locator('#chaticon')).toBeHidden();
await page.locator('label[for="options-disablechat"]').click();
await expect(page.locator('#options-disablechat')).not.toBeChecked();
await expect(page.locator('#chaticon')).toBeVisible();
await hideSettings(page);
await showChat(page);
await expect(page.locator('#chatbox')).toBeVisible();
await expect(page.locator('#chatbox')).toHaveClass(/visible/);
});
// Title-bar layout / glyph regressions from #7590 review.
test('chat title bar lays out as a centred flex row with underscore minimize', async ({page}) => {
await showChat(page);
// Minimize button uses an underscore (sits at the bottom of its em-box and
// reads as a proper minimize indicator); it must not silently revert to
// &minus; or a hyphen.
await expect(page.locator('#titlecross')).toHaveText('_');
const styles = await page.evaluate(() => {
const cs = (sel: string) => getComputedStyle(document.querySelector(sel)!);
const rect = (sel: string) => document.querySelector(sel)!.getBoundingClientRect();
const tb = rect('#titlebar');
const lab = rect('#titlelabel');
const sticky = rect('#titlesticky');
return {
titlebarDisplay: cs('#titlebar').display,
titlebarAlignItems: cs('#titlebar').alignItems,
labelFlex: cs('#titlelabel').flexGrow,
crossFloat: cs('#titlecross').float,
crossTransform: cs('#titlecross').transform,
stickyFloat: cs('#titlesticky').float,
// Visual symmetry — CHAT's left edge sits roughly the same distance
// from the title-bar left edge as the rightmost button sits from the
// right edge. Tested via rendered geometry rather than CSS literal so
// we don't get tripped up by skin overrides (colibris ships its own
// #titlebar padding rule).
leftGap: lab.left - tb.left,
rightGap: tb.right - sticky.right,
};
});
expect(styles.titlebarDisplay).toBe('flex');
expect(styles.titlebarAlignItems).toBe('center');
// Title takes the remaining width so corner buttons sit at the right edge.
expect(styles.labelFlex).toBe('1');
// Buttons are flex items, not floats — old `float: right` layout must stay gone.
expect(styles.crossFloat).toBe('none');
expect(styles.stickyFloat).toBe('none');
// 5px lift on #titlecross so the `_` glyph reads near the title's baseline
// rather than at the very bottom of the row.
expect(styles.crossTransform).not.toBe('none');
// Padding looks symmetric (within 2px to allow for sub-pixel rounding).
expect(Math.abs(styles.leftGap - styles.rightGap)).toBeLessThanOrEqual(2);
});
// Regression: #chaticon was a <div> before the #7584 a11y refactor; once it
// became a <button>, the inner <span class="buttonicon"> being `display: flex`
// (from the global icons.css rule) could intercept clicks and the chat icon
// stopped opening the panel. The fix scopes a reset on `#chaticon .buttonicon`.
test('chat icon click reliably opens the chat box', async ({page}) => {
await expect(page.locator('#chaticon')).toBeVisible();
await page.locator('#chaticon').click();
await expect(page.locator('#chatbox')).toHaveClass(/visible/);
await expect(page.locator('#chatbox')).toBeVisible();
});

View File

@ -160,4 +160,38 @@ test.describe('creator-owned pad settings', () => {
await context2.close();
});
// #7592: ticking "Disable chat" must visibly disable the dependent
// "Chat always on screen" / "Show Chat and Users" toggles, not just
// make the underlying inputs non-interactive.
test('disabling chat disables and visually greys the dependent chat toggles', async ({page}) => {
await goToNewPad(page);
await showSettings(page);
// Initial state: dependent toggles are interactive.
await expect(page.locator('#options-stickychat')).toBeEnabled();
await expect(page.locator('#options-chatandusers')).toBeEnabled();
await page.locator('label[for="options-disablechat"]').click();
await expect(page.locator('#options-disablechat')).toBeChecked();
// Inputs become disabled (refreshMyViewControls in pad.ts).
await expect(page.locator('#options-stickychat')).toBeDisabled();
await expect(page.locator('#options-chatandusers')).toBeDisabled();
// Colibris toggle visualisation dims via opacity:.4 on the label
// (covers the hidden checkbox + before/after pseudo-elements).
const stickyLabelOpacity = await page.evaluate(
() => getComputedStyle(document.querySelector('label[for="options-stickychat"]')!).opacity);
const chatAndUsersLabelOpacity = await page.evaluate(
() => getComputedStyle(document.querySelector('label[for="options-chatandusers"]')!).opacity);
expect(parseFloat(stickyLabelOpacity)).toBeLessThan(1);
expect(parseFloat(chatAndUsersLabelOpacity)).toBeLessThan(1);
// Untick "Disable chat" → dependent toggles are interactive again.
await page.locator('label[for="options-disablechat"]').click();
await expect(page.locator('#options-disablechat')).not.toBeChecked();
await expect(page.locator('#options-stickychat')).toBeEnabled();
await expect(page.locator('#options-chatandusers')).toBeEnabled();
});
});