T9.5 - C1+S8: the help-tip is real, the audit is written, BL-001 is dead
S8, finished where the plan said it would be: every .help-tip badge is a <button> - upgraded by the component itself at load (help.js), with helpTipUpgrade() for late renders, so a badge added tomorrow is born reachable. The count the task warned about came true: 15 at wave 0, 18 at the wave 6 exit, 20 at the start of this task - all 20 buttons now, and the fix being in the component is what stops the number growing again. One viewport-clamped role=tooltip bubble serves every badge: focus shows it, Escape hides it, tap toggles it, tap-elsewhere closes it - the touch path Field View's tablets never had. The injected styles now use theme tokens (four raw hexes of the S5 kind, gone). BL-001, CLOSED after three causes and nine waves: the old CSS ::after escaped its badge to the right and was the creator's last 390px overflow. The clamped bubble ends it - scrollWidth 390 vs clientWidth 390 - and frame_check's pin FLIPPED, exactly as designed: it asserted the failure until the fix landed, and now asserts the fix so a regression reopens the entry loudly. The audit (docs/reference/accessibility-audit.md), every number probe-backed: - div/span click handlers: 12/2 at wave 0 -> 0 (the wizard's constraint library entries and the dashboard chips became buttons here; the comments backdrop stopped pretending to be a control) - outline:none without replacement: 0 (wp-chrome's one is the documented S12 exception - its ring is on :focus-within, one ring not two) - aria-live: every toast system and banner announces - native dialogs: 79 -> 21, all on surfaces no S1 task named (admin, users, launcher) - documented as BL-024 with the T7.9 kit ready for them - keyboard-only primary flow: covered leg by leg by the probes that dispatch real CDP key events, cited in the document Three stale count-pins re-pointed to the numbers this task reached (stepper's baseline-minus-10, form_structure's one-span-left, frame_check's BL-001 pin) - each now pins the TARGET so slack cannot hide a regression. Verification (each probe run alone): NEW tests/helptip_check.py 13/13. Regressions: a11y_check 22/22, stepper_check 71/71, form_structure_check 50/51 (BL-022's product question), pipeline_check 44/44, frame_check 38/38. Items: C1, S8 (BL-001 closed, BL-024 opened) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
121
html/help.js
121
html/help.js
@@ -12,18 +12,121 @@
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
// ── the help-tip component (S8 / T9.5) ─────────────────────────────────────
|
||||
// Markup writes <span class="help-tip" data-tip="…">i</span>; this upgrades
|
||||
// every one to a real <button> at load (and via global.helpTipUpgrade(root)
|
||||
// for anything rendered later). One bubble serves all badges: focus and hover
|
||||
// show it, click/tap toggles it (the touch path tablets need), Escape and
|
||||
// leaving close it. The bubble is clamped to the viewport on both axes.
|
||||
var _tipOpenFor = null;
|
||||
|
||||
function tipBubble() {
|
||||
var b = document.getElementById('wp-tip-bubble');
|
||||
if (!b) {
|
||||
b = document.createElement('div');
|
||||
b.id = 'wp-tip-bubble';
|
||||
b.setAttribute('role', 'tooltip');
|
||||
b.hidden = true;
|
||||
document.body.appendChild(b);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
function tipShow(btn) {
|
||||
var b = tipBubble();
|
||||
b.textContent = btn.getAttribute('data-tip') || '';
|
||||
b.hidden = false;
|
||||
var r = btn.getBoundingClientRect();
|
||||
b.style.left = '0px'; b.style.top = '0px'; // measure at origin
|
||||
var bw = b.offsetWidth, bh = b.offsetHeight;
|
||||
var left = Math.min(Math.max(12, r.left + r.width / 2 - bw / 2),
|
||||
window.innerWidth - bw - 12);
|
||||
var top = r.top - bh - 8;
|
||||
if (top < 8) top = r.bottom + 8;
|
||||
b.style.left = left + 'px';
|
||||
b.style.top = top + 'px';
|
||||
btn.setAttribute('aria-describedby', 'wp-tip-bubble');
|
||||
}
|
||||
|
||||
function tipHide(btn) {
|
||||
var b = document.getElementById('wp-tip-bubble');
|
||||
if (b) b.hidden = true;
|
||||
if (btn) { btn.removeAttribute('aria-describedby'); btn.setAttribute('aria-expanded', 'false'); }
|
||||
if (_tipOpenFor === btn) _tipOpenFor = null;
|
||||
}
|
||||
|
||||
function upgradeTip(el) {
|
||||
if (el.tagName === 'BUTTON') return el;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = el.className;
|
||||
btn.setAttribute('data-tip', el.getAttribute('data-tip') || '');
|
||||
btn.setAttribute('aria-label', 'More information');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.textContent = el.textContent || 'i';
|
||||
el.parentNode.replaceChild(btn, el);
|
||||
return btn;
|
||||
}
|
||||
|
||||
function helpTipUpgrade(root) {
|
||||
(root || document).querySelectorAll('span.help-tip').forEach(upgradeTip);
|
||||
}
|
||||
global.helpTipUpgrade = helpTipUpgrade;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
helpTipUpgrade(document);
|
||||
// Delegated, so badges rendered later work without re-wiring.
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest ? e.target.closest('.help-tip') : null;
|
||||
if (btn && btn.tagName !== 'BUTTON') btn = upgradeTip(btn);
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
if (_tipOpenFor === btn) { tipHide(btn); return; }
|
||||
if (_tipOpenFor) tipHide(_tipOpenFor);
|
||||
_tipOpenFor = btn;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
tipShow(btn);
|
||||
return;
|
||||
}
|
||||
if (_tipOpenFor) tipHide(_tipOpenFor); // tap elsewhere closes
|
||||
});
|
||||
document.addEventListener('focusin', function (e) {
|
||||
var btn = e.target.classList && e.target.classList.contains('help-tip') ? e.target : null;
|
||||
if (btn) tipShow(btn);
|
||||
else if (_tipOpenFor) tipHide(_tipOpenFor);
|
||||
});
|
||||
document.addEventListener('focusout', function (e) {
|
||||
var btn = e.target.classList && e.target.classList.contains('help-tip') ? e.target : null;
|
||||
if (btn && _tipOpenFor !== btn) tipHide(btn);
|
||||
});
|
||||
document.addEventListener('mouseover', function (e) {
|
||||
var btn = e.target.closest ? e.target.closest('.help-tip') : null;
|
||||
if (btn) { if (btn.tagName !== 'BUTTON') btn = upgradeTip(btn); tipShow(btn); }
|
||||
else if (!_tipOpenFor) tipHide(null);
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && _tipOpenFor) tipHide(_tipOpenFor);
|
||||
});
|
||||
});
|
||||
|
||||
// ── styles ────────────────────────────────────────────────────────────────
|
||||
var css = `
|
||||
.help-tip{ display:inline-flex; align-items:center; justify-content:center; width:15px; height:15px;
|
||||
margin-left:5px; border-radius:50%; background:#525252; color:#fff; font-size:10px; font-weight:700;
|
||||
/* S8 / T9.5: the badge is a BUTTON - reachable by keyboard and by touch, which
|
||||
the old span never was (its :focus rule was dead code: no tabindex). The
|
||||
tooltip itself is #wp-tip-bubble below, a positioned element CLAMPED to the
|
||||
viewport - the old ::after escaped its badge to the right and was the last
|
||||
cause of the creator's 390px overflow (BL-001). Colours come from the
|
||||
theme's tokens; this block owned four of the raw hexes S5 counted. */
|
||||
.help-tip{ display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px;
|
||||
margin-left:5px; padding:0; border:0; border-radius:50%;
|
||||
background:var(--cds-icon-secondary); color:var(--cds-text-inverse); font-size:10px; font-weight:700;
|
||||
font-family:ui-sans-serif,system-ui,sans-serif; cursor:help; vertical-align:middle; position:relative; }
|
||||
.help-tip::after{ content:attr(data-tip); position:absolute; bottom:130%; left:50%; transform:translateX(-50%);
|
||||
background:#161616; color:#fff; padding:7px 10px; border-radius:0; font-size:12px; font-weight:400;
|
||||
line-height:1.4; white-space:normal; width:max-content; max-width:260px; text-align:left; z-index:9999;
|
||||
opacity:0; pointer-events:none; transition:opacity .12s; box-shadow:0 4px 14px rgba(20,30,50,.22); }
|
||||
.help-tip::before{ content:''; position:absolute; bottom:130%; left:50%; transform:translate(-50%,95%);
|
||||
border:5px solid transparent; border-top-color:#161616; opacity:0; transition:opacity .12s; z-index:9999; }
|
||||
.help-tip:hover::after, .help-tip:hover::before, .help-tip:focus::after, .help-tip:focus::before{ opacity:1; }
|
||||
.help-tip:focus-visible{ outline:2px solid var(--cds-focus); outline-offset:1px; }
|
||||
.help-tip[aria-expanded="true"]{ background:var(--cds-focus); }
|
||||
#wp-tip-bubble{ position:fixed; z-index:10001; max-width:min(280px, calc(100vw - 24px));
|
||||
background:var(--cds-background-inverse); color:var(--cds-text-inverse);
|
||||
padding:7px 10px; font-size:12px; font-weight:400; line-height:1.4; text-align:left;
|
||||
box-shadow:0 4px 14px rgba(20,30,50,.22); }
|
||||
|
||||
.ui-help-overlay{ position:fixed; inset:0; background:rgba(20,30,50,.5); display:none; align-items:center;
|
||||
justify-content:center; z-index:10000; padding:4vh 16px; }
|
||||
|
||||
@@ -980,11 +980,13 @@ function toggleConstraint(name){
|
||||
function showConstraintLibrary(){
|
||||
const modal = document.getElementById('constraint-modal');
|
||||
const lib = document.getElementById('constraint-library');
|
||||
// C1/T9.5: a library entry is an ACTION, so it is a button - keyboard and
|
||||
// touch come free, and the hover styling moved to CSS where it belongs.
|
||||
lib.innerHTML = CONSTRAINT_LIBRARY.map(c=>`
|
||||
<div class="constraint-option" style="padding:0.75rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; margin-bottom:0.5rem; cursor:pointer; transition:all 0.2s;" onmouseover="this.style.borderColor='var(--primary)'; this.style.background='var(--primary-light)'" onmouseout="this.style.borderColor='var(--border)'; this.style.background='var(--bg)'" onclick="addCustomConstraint('${c}')">
|
||||
<button type="button" class="constraint-option" onclick="addCustomConstraint('${c}')">
|
||||
<strong>${c}</strong>
|
||||
<div style="font-size:12px; color:var(--text-light); margin-top:0.25rem;">Click to add to this project</div>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--text-light); margin-top:0.25rem;">Add to this project</div>
|
||||
</button>
|
||||
`).join('');
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
@@ -741,6 +741,13 @@ body {
|
||||
/* .field-error is declared once, in theme-light.css — the launcher's create form
|
||||
and T5.8's step validation use the same component. */
|
||||
|
||||
/* The constraint-library entries (C1/T9.5): real buttons, block layout. */
|
||||
.constraint-option { display:block; width:100%; text-align:left; padding:0.75rem;
|
||||
background:var(--bg); border:1px solid var(--border); border-radius:6px;
|
||||
margin-bottom:0.5rem; cursor:pointer; font:inherit; color:inherit; transition:all .2s; }
|
||||
.constraint-option:hover, .constraint-option:focus-visible {
|
||||
border-color:var(--primary); background:var(--primary-light); }
|
||||
|
||||
/* NAVIGATION
|
||||
B6 / T7.8: sticky, the creator's pattern. On the Constraints and Sequence
|
||||
steps the proposal's beside-the-fields actions meant scrolling to save; the
|
||||
|
||||
@@ -3409,7 +3409,8 @@ function renderDashboard(){
|
||||
</div>`;
|
||||
|
||||
// status + discipline breakdown chips (status chips also filter the board)
|
||||
const statusChip=(label,count,cls,status)=>`<span class="dash-chip${cls?' '+cls:''}${dashFilter.status===status?' chip-active':''}" onclick="dashSetStatus('${status}')" title="Click to filter the board">${esc(label)}: <b>${count}</b></span>`;
|
||||
// C1/T9.5: the chip filters the board, so it is a button.
|
||||
const statusChip=(label,count,cls,status)=>`<button type="button" class="dash-chip${cls?' '+cls:''}${dashFilter.status===status?' chip-active':''}" onclick="dashSetStatus('${status}')" title="Click to filter the board">${esc(label)}: <b>${count}</b></button>`;
|
||||
const statusChips=STATUS_ORDER.filter(s=>byStatus[s]).map(s=>statusChip(s,byStatus[s],'',s)).join('')
|
||||
+ (byStatus['Issue']?statusChip('On Hold',byStatus['Issue'],'chip-red','Issue'):'');
|
||||
const discChips=Object.keys(byDisc).map(d=>`<span class="dash-chip">${esc(d)}: <b>${byDisc[d]}</b></span>`).join('')||'<span class="dash-chip">—</span>';
|
||||
@@ -3633,7 +3634,9 @@ async function clearMyComments(){ const d=cmtLoad(); const mine=d.comments.filte
|
||||
if(!mine){ toast('No comments to clear.', 'alert'); return; }
|
||||
if(!(await wpConfirmDialog({title:'Clear my comments', message:`Delete your ${mine} comment(s)?`, okLabel:'Delete them'}))) return;
|
||||
d.comments=d.comments.filter(c=>c.clientId!==d.clientId); cmtSave(d); renderComments(); refreshCommentBadges(); }
|
||||
function cmtInit(){ const d=cmtLoad(); cmtSave(d); const a=document.getElementById('cmt-author'); if(a)a.value=d.author||''; cmtUpdateCurStep(); renderComments(); refreshCommentBadges(); }
|
||||
function cmtInit(){
|
||||
const ov=document.getElementById('cmt-overlay');
|
||||
if(ov && !ov._wired){ ov._wired=true; ov.addEventListener('click', toggleComments); } const d=cmtLoad(); cmtSave(d); const a=document.getElementById('cmt-author'); if(a)a.value=d.author||''; cmtUpdateCurStep(); renderComments(); refreshCommentBadges(); }
|
||||
|
||||
// ── STATUS PILLS ─────────────────────────────────────────────────────────────
|
||||
document.querySelectorAll('#status-group .radio-pill').forEach(p=>p.addEventListener('click',e=>{
|
||||
|
||||
@@ -591,7 +591,9 @@
|
||||
</div>
|
||||
|
||||
<!-- COMMENTS DRAWER -->
|
||||
<div class="cmt-overlay" id="cmt-overlay" onclick="toggleComments()"></div>
|
||||
<!-- The backdrop is NOT a control (C1): pointer dismissal is attached in
|
||||
cmtInit(), and Escape + the drawer's close button are the real paths. -->
|
||||
<div class="cmt-overlay" id="cmt-overlay"></div>
|
||||
<aside class="cmt-drawer" id="cmt-drawer" aria-hidden="true">
|
||||
<div class="cmt-head"><div class="cmt-title">Review Comments</div><button class="cmt-x" onclick="toggleComments()" title="Close">✕</button></div>
|
||||
<div class="cmt-namebar"><label>Your name</label><input type="text" id="cmt-author" placeholder="e.g. J. Park" oninput="cmtSaveAuthor(this.value)"></div>
|
||||
|
||||
@@ -1115,7 +1115,8 @@
|
||||
.dash-metric[onclick] { cursor:pointer; transition:border-color .12s, box-shadow .12s; }
|
||||
.dash-metric[onclick]:hover { border-color:var(--accent); }
|
||||
.dash-metric.dm-active { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-dim); }
|
||||
.dash-chip[onclick] { cursor:pointer; }
|
||||
/* Chips are buttons since T9.5; reset the button chrome, keep the chip look. */
|
||||
button.dash-chip { font:inherit; font-size:12px; cursor:pointer; }
|
||||
.dash-chip.chip-active { border-color:var(--accent); color:var(--accent); background:var(--accent-dim); }
|
||||
.dash-breakdown { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px; }
|
||||
.dash-bd-title { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--text-muted); margin-bottom:6px; }
|
||||
|
||||
Reference in New Issue
Block a user