Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions test/ui-actions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'

const cardSource = readFileSync(new URL('../ui/ContributionCard.jsx', import.meta.url), 'utf8')
const stackSource = readFileSync(new URL('../ui/ContributionStack.jsx', import.meta.url), 'utf8')

test('send actions keep a visible label instead of relying on the icon alone', () => {
assert.match(cardSource, /sending \? 'Sending…' : 'Send'/)
assert.match(stackSource, /<span>Send<\/span>/)
})

test('single and stacked sends expose elapsed progress to assistive technology', () => {
for (const source of [cardSource, stackSource]) {
assert.match(source, /role="status" aria-live="polite"/)
assert.match(source, /sendElapsed/)
}
})
4 changes: 4 additions & 0 deletions theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@ export const CSS = `
font: inherit; cursor: pointer;
transition: color .14s ease, border-color .14s ease, background .14s ease, transform .1s ease;
}
.co-send-btn {
width: auto; min-width: 76px; padding: 0 12px; gap: 7px;
font-size: 12.5px; font-weight: 700;
}
.co-icon-btn.is-primary {
border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
background: color-mix(in srgb, var(--accent) 11%, var(--surface));
Expand Down
26 changes: 24 additions & 2 deletions ui/ContributionCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ function ReviewPlan({ rec, loadDiff }) {
function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) {
const [sendNote, setSendNote] = useState(null)
const [sending, setSending] = useState(false)
const [sendElapsed, setSendElapsed] = useState(0)
const [dismissing, setDismissing] = useState(false)
// Dismiss abandons the prepared record (a CAS flip the skill treats as
// terminal), so it must never fire on a single stray tap. The first tap arms
Expand All @@ -252,6 +253,18 @@ function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) {
if (confirmingDismiss) keepButtonRef.current?.focus()
}, [confirmingDismiss])

useEffect(() => {
if (!sending) {
setSendElapsed(0)
return undefined
}
const startedAt = Date.now()
const update = () => setSendElapsed(Math.floor((Date.now() - startedAt) / 1000))
update()
const timer = window.setInterval(update, 1000)
return () => window.clearInterval(timer)
}, [sending])

async function send() {
if (!isPr || blocked) return
setSending(true)
Expand Down Expand Up @@ -343,7 +356,7 @@ function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) {
{isPr ? (
<button
type="button"
className="co-icon-btn is-primary"
className="co-icon-btn co-send-btn is-primary"
disabled={sending || blocked}
onClick={send}
aria-label={sending
Expand All @@ -353,7 +366,10 @@ function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) {
: 'Send pull request for review'}
title={blocked ? 'Fresh review required' : 'Send for review'}
>
{sending ? <span className="co-action-spinner" aria-hidden="true" /> : <Icon name="send" />}
{sending
? <span className="co-action-spinner" aria-hidden="true" />
: <Icon name="send" />}
<span>{sending ? 'Sending…' : 'Send'}</span>
</button>
) : null}
<button
Expand Down Expand Up @@ -385,6 +401,12 @@ function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) {
Sending is paused until the updated version is reviewed again.
</p>
) : null}
{sending && (
<p className="co-review-note" role="status" aria-live="polite">
Checking the reviewed source and publishing it to GitHub
{sendElapsed >= 5 ? ` · ${sendElapsed}s elapsed` : '…'}
</p>
)}
{sendNote && <p className="co-review-note">{sendNote}</p>}
{note && <p className="co-review-error">{note}</p>}
</>
Expand Down
22 changes: 21 additions & 1 deletion ui/ContributionStack.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function ContributionStack({
}) {
const [confirming, setConfirming] = useState(false)
const [sending, setSending] = useState(false)
const [sendElapsed, setSendElapsed] = useState(0)
const [note, setNote] = useState('')
const progress = stackProgress(unit)
const ready = unit.records.filter((rec) => rec.status === 'prepared')
Expand All @@ -54,6 +55,18 @@ export function ContributionStack({
if (!canSend && confirming) setConfirming(false)
}, [canSend, confirming])

useEffect(() => {
if (!sending) {
setSendElapsed(0)
return undefined
}
const startedAt = Date.now()
const update = () => setSendElapsed(Math.floor((Date.now() - startedAt) / 1000))
update()
const timer = window.setInterval(update, 1000)
return () => window.clearInterval(timer)
}, [sending])

async function send() {
if (!canSend) return
setSending(true)
Expand Down Expand Up @@ -167,12 +180,18 @@ export function ContributionStack({
{sending ? 'Sending…' : 'Send for review'}
</button>
</div>
{sending ? (
<p className="co-review-note" role="status" aria-live="polite">
Publishing the reviewed pull requests in order
{sendElapsed >= 5 ? ` · ${sendElapsed}s elapsed` : '…'}
</p>
) : null}
</div>
) : (
<div className="co-stack-actions">
<button
type="button"
className="co-icon-btn is-primary"
className="co-icon-btn co-send-btn is-primary"
disabled={!canSend}
aria-label={blocked > 0 ? 'Fresh review required before sending' : 'Send related changes for review'}
title={blocked > 0 ? 'Fresh review required' : 'Send for review'}
Expand All @@ -184,6 +203,7 @@ export function ContributionStack({
onClick={() => setConfirming(true)}
>
<Icon name="send" />
<span>Send</span>
</button>
<button
type="button"
Expand Down
Loading