mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-23 05:11:34 +00:00
* fix(v3): reject accidental text-selection of UI chrome (user-select policy)
Dragging/double-clicking across the v3 UI marquee-highlighted buttons, labels,
the sidebar, transport, and the note-highway HUD — looks broken (reported Mac +
Windows). Default the v3 shell to user-select:none on html, then opt CONTENT
back in. Decided by a 4-lens panel (UX / a11y / dev-ops / plugin-ecosystem);
their guardrails are baked in:
- Form fields ALWAYS re-enabled (input/textarea/select/[contenteditable]) so the
caret + IME composition never break. No `* { user-select:none }` (WebKit input
bug 82692).
- Plugin screens (.screen[id^="plugin-"]) stay selectable BY INHERITANCE (no `*`,
so a plugin's own non-select chrome still wins) — a plugin's copyable text
(lyrics, chords, results), including community/out-of-tree plugins that never
adopt the class, isn't silently locked.
- Core read-only content opts back in by CONTAINER via a hand-authored
`.fb-selectable` (not a Tailwind utility — so runtime-installed plugins get it
too): the whole Settings panel (paths, device names, version, diagnostics,
About) and the now-playing song metadata. Answers the open "keep settings
copyable?" question: yes, at the container.
Cosmetic only — never used to lock copy-worthy text (errors/IDs/paths/versions/
metadata stay selectable; WCAG 2.2 allows copy-paste as a mechanism). v3-only
(v2 unchanged; v3.css loads only on /v3); plain CSS, no Tailwind rebuild; no
desktop/Electron changes (standard OS-framed window). `.fb-selectable` is
documented in CLAUDE.md for plugin authors.
Tests: tests/js/v3_user_select_policy.test.js (html default, form-field
re-enable, plugin-screen carve without `*`, .fb-selectable, container opt-ins,
and the no-`*`-rule guardrail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* fix(v3): address review of the user-select policy (#637)
Review (manual + Codex) of the v3 text-selection policy:
- P1 (real bug): the now-playing HUD metadata opted into `.fb-selectable` but
its `#player-hud` parent is `pointer-events: none`, so the mouse could never
reach the text to select it — the opt-in was inert. Add `pointer-events-auto`
to the metadata block (verified in-browser: user-select:text + pointer-
events:auto, while the HUD parent stays pointer-events:none).
- Coverage: the PR's a11y guardrail promised copyable text stays selectable
"incl. in modals/toasts", but only Settings + the HUD were opted in. Blanket-
opt the focused copyable surfaces back in by selector — `.feedBack-modal`,
`[role="dialog"]`, `#fb-notify-stack`, `#v3-fb-toast`, `#scan-banner` — so
errors / IDs / paths / file names in dialogs, toasts, and the scan banner stay
copyable. These are focused panels, not dense card lists, so re-enabling
selection there can't recreate the across-cards marquee mess.
(Deliberately NOT opting in the library grid / dashboard / profile card lists:
making dense card text selectable would reintroduce exactly that marquee mess
on a drag — copy song metadata from the now-playing HUD / Settings instead.)
- Test (P3): assert the selectable rule's selectors order-independently, cover
the new modal/toast/banner surfaces, and check the HUD block carries BOTH
fb-selectable and pointer-events-auto (class-order independent).
Verified in a real browser (chromium): html=none, sidebar chrome=none, input=
text, Settings=text, HUD meta=text+pointer-events:auto, dialog/modal=text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
81 lines
4.1 KiB
JavaScript
81 lines
4.1 KiB
JavaScript
// Guards the v3 text-selection policy (static/v3/v3.css + static/v3/index.html):
|
|
// the UI defaults to non-selectable so accidental chrome selection can't look
|
|
// broken, while form fields, plugin screens, and core content opt back in. A
|
|
// future global reset clobbering the rule — or the content containers losing
|
|
// their .fb-selectable opt-in — should fail here.
|
|
//
|
|
// Source-level only — same strategy as the other tests/js/ files.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const root = path.join(__dirname, '..', '..');
|
|
// Strip block comments so the policy's own explanatory prose (which quotes the
|
|
// `* { user-select:none }` anti-pattern as a warning) can't trip the assertions.
|
|
const css = fs.readFileSync(path.join(root, 'static', 'v3', 'v3.css'), 'utf8')
|
|
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
const html = fs.readFileSync(path.join(root, 'static', 'v3', 'index.html'), 'utf8');
|
|
|
|
test('v3 defaults to non-selectable on html (not a universal `*` rule)', () => {
|
|
assert.match(css, /html\s*\{[^}]*user-select:\s*none/,
|
|
'html must default user-select: none');
|
|
// The `* { user-select: none }` anti-pattern breaks input carets / IME — must not exist.
|
|
assert.doesNotMatch(css, /\*\s*\{[^}]*user-select:\s*none/,
|
|
'must NOT use a universal `*` user-select:none rule');
|
|
});
|
|
|
|
test('form fields are always re-enabled (caret / IME safe)', () => {
|
|
assert.match(
|
|
css,
|
|
/input,\s*textarea,\s*select[\s\S]*?contenteditable[\s\S]*?user-select:\s*text/,
|
|
'input/textarea/select/[contenteditable] must be re-enabled to user-select: text',
|
|
);
|
|
});
|
|
|
|
test('plugin screen subtree stays selectable by inheritance (no `*`, respects plugin opt-outs)', () => {
|
|
assert.match(
|
|
css,
|
|
/\.screen\[id\^="plugin-"\]\s*\{[^}]*user-select:\s*text/,
|
|
'plugin screens must be re-enabled so plugin content is not silently un-copyable',
|
|
);
|
|
assert.doesNotMatch(
|
|
css,
|
|
/\.screen\[id\^="plugin-"\]\s*\*/,
|
|
'the plugin carve must NOT use `*` (would override a plugin\'s own non-select chrome)',
|
|
);
|
|
});
|
|
|
|
// The rule that re-enables selection on copyable content. Find the single
|
|
// declaration block whose body sets `user-select: text`, then assert each
|
|
// required selector is one of its selectors — order/format independent.
|
|
const selectableRule = (css.match(/([^{}]*)\{[^}]*user-select:\s*text[^}]*\}/g) || [])
|
|
.join('\n');
|
|
|
|
test('core content opts back in via .fb-selectable (element + descendants)', () => {
|
|
assert.match(selectableRule, /\.fb-selectable\b/, '.fb-selectable must set user-select: text');
|
|
assert.match(selectableRule, /\.fb-selectable\s*\*/, '...and its descendants (.fb-selectable *)');
|
|
});
|
|
|
|
test('focused copyable surfaces (modals/toasts/scan banner) opt back in', () => {
|
|
// The PR\'s a11y guardrail keeps copyable text selectable "incl. in
|
|
// modals/toasts" — these carry errors / IDs / paths the user copies.
|
|
assert.match(selectableRule, /\.feedBack-modal\b/, 'modals (.feedBack-modal) must be selectable');
|
|
assert.match(selectableRule, /\[role="dialog"\]/, 'dialogs ([role="dialog"]) must be selectable');
|
|
assert.match(selectableRule, /#fb-notify-stack\b/, 'toasts (#fb-notify-stack) must be selectable');
|
|
assert.match(selectableRule, /#scan-banner\b/, 'the scan banner (#scan-banner) must be selectable');
|
|
});
|
|
|
|
// Match a class="" attribute that contains ALL given tokens in any order.
|
|
const hasClasses = (...tokens) => new RegExp(
|
|
'class="' + tokens.map((t) => '(?=[^"]*\\b' + t + '\\b)').join('') + '[^"]*"');
|
|
|
|
test('the Settings panel and now-playing metadata carry .fb-selectable', () => {
|
|
assert.match(html, hasClasses('fb-settings', 'fb-selectable'),
|
|
'the Settings panel must opt back in (paths / version / diagnostics / About)');
|
|
assert.match(html, hasClasses('fb-selectable', 'pointer-events-auto'),
|
|
'the now-playing metadata must opt back in AND re-enable pointer-events '
|
|
+ '(its #player-hud parent is pointer-events-none, which would block mouse selection)');
|
|
});
|