Skip to content

Replace native alert()/confirm() with tpen-alert/tpen-confirm modal dialogs#496

Merged
cubap merged 13 commits intomainfrom
copilot/refactor-alert-confirm-dialogs
Mar 5, 2026
Merged

Replace native alert()/confirm() with tpen-alert/tpen-confirm modal dialogs#496
cubap merged 13 commits intomainfrom
copilot/refactor-alert-confirm-dialogs

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 23, 2026

Native alert() and confirm() calls produce inconsistent, browser-controlled blocking dialogs. This replaces all instances across the codebase with the existing tpen-alert / tpen-confirm event-dispatch system.

Alert replacements

alert()TPEN.eventDispatcher.dispatch('tpen-alert', { message })

Affected: annotorious-annotator/plain.js, roles-handler, projects/index.js, project-list-write.js, api/Project.js, manage-project/index.js

The permissions-denied alert in manage-project/index.js now defers the redirect via tpen-alert-acknowledged so the user sees the dialog before being navigated away.

Confirm replacements

All confirm() calls are replaced via a new utilities/confirmAction.js helper:

confirmAction(message, onConfirm, onCancel, { positiveButtonText, negativeButtonText })

The helper generates a unique confirmId (crypto.randomUUID() with a fallback) for each dialog. ConfirmContainer echoes the ID through button-click events; listeners use on() with an ID guard so stale callbacks from earlier dialogs never fire on a later one.

Affected: decline-project, leave-project, manage-layers, manage-pages, manifest-import, navigation-manager, quicktype-manager, roles-handler (3 instances), line-parser.js (3 instances), manage-project/index.js

ConfirmContainer improvements

  • Stacking prevention: ignores new tpen-confirm requests while a dialog is already visible, preventing button-mash stacking.
  • Keyboard accessibility: auto-focuses the positive button when a dialog opens; added :focus-visible CSS so the focused button has a visible outline, making tab/enter navigation work correctly.
  • Contextual button labels: all call sites pass explicit positiveButtonText/negativeButtonText. Destructive or statement-style confirms use specific labels (e.g., Delete/Cancel, Leave/Cancel, Publish/Cancel, Transfer/Cancel, Discard/Keep Editing); question-style confirms keep the Yes/No defaults.

Methods that were async solely due to await confirm(…) patterns have had async/await removed from both the method and its call sites.

Original prompt

This section details on the original issue you should resolve

<issue_title>alert() and confirm() where a dialog or modal makes more sense</issue_title>
<issue_description>There are a few things noted here that are out of scope but should be handled somewhere else.
...
3. Old pattern has alert() and confirm() where a dialog or modal makes more sense.</issue_description>

Comments on the Issue (you are @copilot in this section)

@thehabes See #327

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: cubap <1119165+cubap@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace alert() and confirm() with dialog or modal Replace native alert()/confirm() with tpen-alert/tpen-confirm modal dialogs Feb 23, 2026
Copilot AI requested a review from cubap February 23, 2026 16:03
Copy link
Copy Markdown
Member

@cubap cubap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spot checks and readthrough pass.

Image

@cubap cubap requested a review from Copilot February 23, 2026 21:37
@cubap cubap marked this pull request as ready for review February 23, 2026 21:38
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request modernizes user interaction by replacing native browser alert() and confirm() dialogs with custom modal dialogs using the existing TPEN event dispatcher system. Native dialogs produce inconsistent, browser-controlled blocking UI that varies across browsers. The new implementation provides a consistent, styled modal experience across the application.

Changes:

  • Replaced all alert() calls with TPEN.eventDispatcher.dispatch('tpen-alert', { message }) for non-blocking custom alert modals
  • Replaced all confirm() calls with an async event-dispatch pattern using cross-removing one() listener pairs to handle positive/negative responses
  • Removed unnecessary async/await keywords from methods that were only async due to confirm() usage

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
File Description
interfaces/manage-project/index.js Replaced confirm dialog for manifest export and alert for permission check
components/roles-handler/index.js Replaced alert for errors and confirms for unsaved changes, ownership transfer, and member removal
components/quicktype-manager/index.js Replaced confirm dialog for clearing shortcuts
components/projects/project-list-write.js Replaced alert for delete not implemented message
components/projects/index.js Replaced alert for delete not implemented message
components/navigation-manager/index.js Replaced confirm dialog for resetting navigation URLs
components/manifest-import/index.js Replaced confirm dialog for multi-manifest import
components/manage-pages/index.js Replaced confirm dialog for page deletion
components/manage-layers/index.js Replaced confirm dialog for layer deletion
components/leave-project/index.js Replaced confirm dialog for leaving project
components/decline-project/index.js Replaced confirm dialog for declining invitation
components/annotorious-annotator/plain.js Replaced alert for missing pageID and confirm for annotation removal
components/annotorious-annotator/line-parser.js Replaced confirms for navigation with unsaved changes, annotation removal, and delete all annotations; removed async/await
api/Project.js Replaced alert for role update failure

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Member

@thehabes thehabes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

The 'Yes' and 'No' do not always fit the wording, we will have to go through some of these. When active I can never tab-select yes or no. These should be accessible via keyboard-only means where tab selects the buttons, there is some selected/focused css, and 'enter' works to make the selection.

Image

I can button mash and cause multiple dialogues. They are not cleared when you click 'no', so consecutive calls continue to show old dialogues.

image

This one is not actionable.

Fixed a small UX effect where activating the dialogue caused the page size to visually bump. This is because it would cause the vertical scrollbar to hide.

Static Review Comments

Branch: copilot/refactor-alert-confirm-dialogs
Review Date: 2026-02-24
Reviewer: Claude Code assisted by Bryan - @thehabes

Bryan and Claude make mistakes. Verify all issues and suggestions. Avoid unnecessary scope creep.

Summary

The replacement pattern is mechanically correct and applied consistently. The primary concern is a race condition inherent to using a global event bus for dialog responses.

Category Issues Found
🔴 Critical 1
🟠 Major 0
🟡 Minor 2
🔵 Suggestions 1

Critical Issues 🔴

Issue 1: Race condition — global event bus confirm listeners collide

File: All files using the tpen-confirm pattern
Category: Logic Error

Problem:
The confirm pattern uses the global eventDispatcher singleton for both dispatching the dialog and listening for the response. If two components fire tpen-confirm in quick succession (or a user triggers a confirm while one is already showing), the one() listeners from the first confirm are still registered when the second confirm's buttons fire. The cross-removal (off()) only cleans up the paired handler — it doesn't prevent a different component's stale handlers from also firing.

Consider this sequence:

  1. Component A dispatches tpen-confirm and registers onPositive_A / onNegative_A
  2. Before the user responds, Component B dispatches tpen-confirm and registers onPositive_B / onNegative_B
  3. User clicks "Yes" on Component B's dialog
  4. tpen-confirm-positive fires — both onPositive_A and onPositive_B execute (both registered via one())

This means Component A's destructive action (delete, leave project, transfer ownership, etc.) executes without user intent.

How severe this is in practice: The ConfirmContainer does not queue dialogs — calling addConfirm() appends a new <tpen-confirm> element to the container. But the buttons for both dialogs dispatch the same tpen-confirm-positive/tpen-confirm-negative events, so any button click resolves all pending listeners. This is especially dangerous for irreversible operations like deleteAllAnnotations, transferOwnership, and removeMember.

Suggested Fix:
This is an architectural concern that may be out of scope for this PR, since the tpen-confirm event system predates these changes. However, the migration from synchronous confirm() (which inherently blocked and prevented races) to async events introduces this risk. A dialog ID or correlation token would solve it:

// Dispatch with a unique ID
const confirmId = crypto.randomUUID()
const onPositive = (ev) => {
    if (ev.detail?.confirmId !== confirmId) return
    TPEN.eventDispatcher.off('tpen-confirm-negative', onNegative)
    // perform action
}
const onNegative = (ev) => {
    if (ev.detail?.confirmId !== confirmId) return
    TPEN.eventDispatcher.off('tpen-confirm-positive', onPositive)
}
TPEN.eventDispatcher.on('tpen-confirm-positive', onPositive)
TPEN.eventDispatcher.on('tpen-confirm-negative', onNegative)
TPEN.eventDispatcher.dispatch('tpen-confirm', { message: '...', confirmId })

The ConfirmContainer would need to pass the confirmId through to the button handlers and include it in the dispatched events. This could be a follow-up issue.

How to Verify:

  1. Open a page that has two confirm-triggering buttons visible
  2. Use browser console to rapidly dispatch two tpen-confirm events
  3. Click "Yes" on the second dialog and observe whether the first handler also fires

Major Issues 🟠

None


Minor Issues 🟡

Issue 1: confirm() dialog message reads like a statement, not a question

File: components/roles-handler/index.js:716
Category: UX / Copy

Problem:
The confirm message "This action will remove ${memberName} from your project. Click 'OK' to continue?" references "OK" but the tpen-confirm dialog uses "Yes"/"No" buttons:

TPEN.eventDispatcher.dispatch('tpen-confirm', {
    message: `This action will remove ${memberName} from your project. Click 'OK' to continue?`
})

Suggested Fix:

TPEN.eventDispatcher.dispatch('tpen-confirm', {
    message: `This action will remove ${memberName} from your project. Do you want to continue?`
})

Issue 2: manage-project/index.js alert fires then immediately redirects

File: interfaces/manage-project/index.js:74-75
Category: UX / Logic

Problem:
When the user lacks permission, the code dispatches a tpen-alert and then immediately sets document.location.href. The alert is non-blocking now, so the redirect will fire before the user sees the dialog:

TPEN.eventDispatcher.dispatch('tpen-alert', { message: "You do not have permissions to use this page." })
document.location.href = `/project?projectID=${TPEN.screen.projectInQuery}`

With native alert() this worked because alert() blocked execution. With the event-based approach, the redirect fires immediately.

Suggested Fix:
Use tpen-alert-acknowledged to defer the redirect:

TPEN.eventDispatcher.dispatch('tpen-alert', { message: "You do not have permissions to use this page." })
TPEN.eventDispatcher.one('tpen-alert-acknowledged', () => {
    document.location.href = `/project?projectID=${TPEN.screen.projectInQuery}`
})

How to Verify:

  1. Navigate to /project/manage?projectID=... as a user without edit permissions
  2. Observe whether the alert message is visible before the redirect occurs

Suggestions 🔵

Suggestion 1: Extract a confirmAction helper to reduce boilerplate

Category: DRY / Maintainability

The 10-line confirm pattern is repeated identically ~14 times. A helper would reduce duplication and make the race condition fix (Issue 🔴 1) a single-point change:

// e.g., in api/events.js or a utility file
export function confirmAction(message, onConfirm, onCancel) {
    const onPositive = () => {
        eventDispatcher.off('tpen-confirm-negative', onNegative)
        onConfirm()
    }
    const onNegative = () => {
        eventDispatcher.off('tpen-confirm-positive', onPositive)
        onCancel?.()
    }
    eventDispatcher.one('tpen-confirm-positive', onPositive)
    eventDispatcher.one('tpen-confirm-negative', onNegative)
    eventDispatcher.dispatch('tpen-confirm', { message })
}

Usage becomes:

confirmAction("Delete all annotations?", () => {
    // destructive action here
})

This would also be the natural place to add the confirmId correlation token from Issue 🔴 1.

@cubap
Copy link
Copy Markdown
Member

cubap commented Feb 26, 2026

@copilot Review comments... There may be some places we might consider an OK/Cancel option instead of just Yes/No:
Image
The 'Yes' and 'No' do not always fit the wording, we will have to go through some of these. When active I can never tab-select yes or no. These should be accessible via keyboard-only means where tab selects the buttons, there is some selected/focused css, and 'enter' works to make the selection.

…rm dialogs

Co-authored-by: cubap <1119165+cubap@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 26, 2026

Image

The 'Yes' and 'No' do not always fit the wording, we will have to...

Addressed in commit cfce025:

  • Race condition: Extracted utilities/confirmAction.js — each call generates a confirmId (via crypto.randomUUID() with fallback). ConfirmContainer echoes the ID through button-click events; listeners use on() with an ID guard so stale callbacks from earlier dialogs never fire.
  • Stacked dialogs / button mashing: ConfirmContainer.addConfirm() now checks for an existing tpen-confirm element and ignores new requests while one is active.
  • Keyboard accessibility: ConfirmContainer auto-focuses the positive button when the dialog opens; added :focus-visible CSS so the focused button has a visible outline.
  • Non-blocking alert + redirect (manage-project/index.js): redirect is now deferred behind tpen-alert-acknowledged so the user sees the alert before being sent away.
  • "Click OK" wording: updated to "Do you want to continue?" in roles-handler.
  • Button text: all 14 confirm call sites now pass descriptive positiveButtonText/negativeButtonText (e.g., Delete/Cancel, Leave/Cancel, Publish/Cancel, Discard/Keep Editing, etc.) via the new confirmAction() helper.

@copilot Review comments... There may be some places we might consider an OK/Cancel option instead of just Yes/No:
Image
The 'Yes' and 'No' do not always fit the wording, we will have to go through so...

Done in commit cfce025. All 14 confirm call sites now pass explicit positiveButtonText/negativeButtonText via the new confirmAction() helper. Statement-style and destructive-action confirms use specific labels (e.g., "Delete/Cancel", "Leave/Cancel", "Publish/Cancel", "Transfer/Cancel", "Discard/Keep Editing"), while question-style confirms keep the Yes/No defaults. Tab navigation now works correctly — the positive button receives focus automatically when the dialog opens, and the :focus-visible CSS makes the focused button clearly visible.

Copilot AI requested a review from cubap February 26, 2026 21:17
Introduce confirmAction in api/events.js to centralize confirm dialog handling with unique confirmId to avoid race conditions. Revamp ConfirmContainer to queue dialogs, handle keyboard navigation, manage focus, and dispatch confirm events with confirmId. Improve AlertContainer with keyboard handling and focus management. Replace many ad-hoc TPEN.eventDispatcher one/off/dispatch confirm patterns across components (annotorious-annotator, plain, decline-project, leave-project, manage-layers, manage-pages, manifest-import, navigation-manager, quicktype-manager, roles-handler, manage-project interface) to use confirmAction, simplifying code and reducing listener collisions. Also adjust redirect behavior after alerts in manage-project to wait for acknowledgement.
@cubap
Copy link
Copy Markdown
Member

cubap commented Mar 2, 2026

Hey @static-reviewer please take a look at this

@habesoftware-claude-connector
Copy link
Copy Markdown

habesoftware-claude-connector Bot commented Mar 2, 2026

Claude Code Review -- Complete

Review report posted below.

@habesoftware-claude-connector
Copy link
Copy Markdown

Now I have all the context needed for a thorough review. Let me compile the report.

Static Review Comments

Branch: copilot/refactor-alert-confirm-dialogs
Review Date: 2026-03-02
Reviewer: Claude Code assisted by Bryan - @thehabes

Bryan and Claude make mistakes. Verify all issues and suggestions. Avoid unnecessary scope creep.

Summary

This PR replaces native alert() and confirm() calls with custom tpen-alert / tpen-confirm modal dialog components across 18 files. A new confirmAction() utility function provides a callback-based API with unique correlation IDs to prevent cross-fire between concurrent dialogs. The ConfirmContainer was enhanced with stacking prevention, accessibility improvements (focus management, prefers-reduced-motion), and a race-condition fix. The changes convert synchronous blocking dialogs to asynchronous event-driven ones, which is a significant behavioral shift.

Category Issues Found
🔴 Critical 1
🟠 Major 4
🟡 Minor 5
🔵 Suggestions 4

Critical Issues 🔴

Issue 1: XSS via unsanitized user message injected into innerHTML

File: components/decline-project/index.js:108 and components/leave-project/index.js:166
Category: Security

Problem:
The server response userMessage is injected directly into innerHTML without sanitization. If the API response contains user-controlled content (e.g., project titles, error messages), this is an XSS vector. The same pattern appears in the error path at lines 116/174.

Current Code:

// decline-project/index.js:108
this.shadowRoot.innerHTML = `
    <h3> ${userMessage} </h3>
`

// leave-project/index.js:174 (error path)
this.shadowRoot.innerHTML = `
    ...
    <code>${userMessage}<code>
`

Suggested Fix:

// Import escapeHtml from utils.js
import { escapeHtml } from '/js/utils.js'

// Then sanitize:
this.shadowRoot.innerHTML = `
    <h3>${escapeHtml(userMessage)}</h3>
`

How to Verify:

  1. Simulate an API response containing <img src=x onerror=alert(1)> as the message
  2. Confirm the HTML entities are escaped and no script executes

Major Issues 🟠

Issue 1: Unclosed <code> tags (malformed HTML)

File: components/decline-project/index.js:122 and components/leave-project/index.js:180
Category: Syntax Error

Problem:
Both error-path templates have <code> tags that are never properly closed — they use <code> instead of </code>.

Current Code:

// decline-project/index.js:122
<code> ${userMessage} <code>

// leave-project/index.js:180
<code>${userMessage}<code>

Suggested Fix:

// decline-project/index.js:122
<code>${escapeHtml(userMessage)}</code>

// leave-project/index.js:180
<code>${escapeHtml(userMessage)}</code>

How to Verify:
Trigger the error path (e.g., by simulating a failed API call) and inspect the rendered DOM for proper <code> tag closure.


Issue 2: role="confirm" is not a valid ARIA role

File: components/gui/confirm/Confirm.js:17
Category: Accessibility

Problem:
The <output> element uses role="confirm", which is not a valid ARIA role. Unlike role="alert" (used correctly in Alert.js), "confirm" is not recognized by assistive technologies. This means screen readers won't announce the dialog content properly.

Current Code:

<output role="confirm">
    <slot></slot>
</output>

Suggested Fix:

<output role="alertdialog">
    <slot></slot>
</output>

alertdialog is the standard ARIA role for dialogs that require user confirmation. Alternatively, role="dialog" would also be valid.

How to Verify:

  1. Open a confirm dialog while using a screen reader (VoiceOver/NVDA)
  2. Confirm the dialog content is announced when it appears

Issue 3: Confirm dialog silently drops new requests while one is visible

File: components/gui/confirm/ConfirmContainer.js:60
Category: Logic Error / UX

Problem:
When a confirm dialog is already showing and a second tpen-confirm event fires, the second request is silently ignored (line 60). The caller's confirmAction() listeners remain registered on the eventDispatcher but will never fire — they listen for tpen-confirm-positive / tpen-confirm-negative events that will never be dispatched for the dropped dialog. This means the callback and event listeners for the second confirm are leaked — they stay registered forever.

Current Code:

addConfirm(message, positiveButtonText, negativeButtonText, confirmId) {
    if (!message || typeof message !== 'string') return
    // Prevent multiple dialogs from stacking when actions are triggered rapidly
    if (this.#screenLockingSection.querySelector('tpen-confirm')) return
    // ...
}

Suggested Fix:
When a second confirm is requested while one is active, dispatch a negative response for the dropped confirmId so the caller's listeners are cleaned up:

addConfirm(message, positiveButtonText, negativeButtonText, confirmId) {
    if (!message || typeof message !== 'string') return
    if (this.#screenLockingSection.querySelector('tpen-confirm')) {
        // Clean up the dropped request's listeners by firing a negative response
        if (confirmId) {
            eventDispatcher.dispatch("tpen-confirm-negative", { confirmId })
        }
        return
    }
    // ...
}

How to Verify:

  1. Rapidly trigger two confirm actions in sequence
  2. Confirm the second request's onCancel callback fires (or at minimum, listeners are cleaned up)

Issue 4: onElement called with potentially null element

File: components/navigation-manager/index.js:372
Category: Runtime Error

Problem:
The save button (#save-btn) is only rendered conditionally when hasUnsavedChanges() returns true (line 340). But addEventListeners() always tries to attach a listener to it (line 372). When there are no unsaved changes, saveBtn is null, and this.renderCleanup.onElement(null, 'click', ...) will throw.

Current Code:

// Line 340 (render): conditionally rendered
${this.hasUnsavedChanges() ? '<button class="btn btn-primary" id="save-btn">Save Changes</button>' : ''}

// Line 372 (addEventListeners): always tries to bind
const saveBtn = this.shadowRoot.querySelector('#save-btn')
this.renderCleanup.onElement(saveBtn, 'click', () => this.saveNavigation())

Suggested Fix:

const saveBtn = this.shadowRoot.querySelector('#save-btn')
if (saveBtn) {
    this.renderCleanup.onElement(saveBtn, 'click', () => this.saveNavigation())
}

How to Verify:

  1. Load the navigation manager with no unsaved changes
  2. Confirm the component renders without console errors

Minor Issues 🟡

Issue 1: originalRoles is mutated by .sort()

File: components/roles-handler/index.js:580-582
Category: Logic Error (subtle)

Problem:
Array.sort() sorts in place. this.originalRoles.sort().join(",") permanently mutates this.originalRoles, which can cause subsequent hasChanges() calls to compare against the sorted version rather than the original insertion order. While the comparison still works (both are sorted), the mutation is unintentional and could cause confusion in debugging.

Current Code:

const currentSelection = currentRoles.sort().join(",")
const originalSelection = this.originalRoles.sort().join(",")

Suggested Fix:

const currentSelection = [...currentRoles].sort().join(",")
const originalSelection = [...this.originalRoles].sort().join(",")

The same pattern appears at lines 739-740 in handleModalClose.


Issue 2: leaveBtn disabled text set via setAttribute("value") instead of textContent

File: components/leave-project/index.js:150 and components/decline-project/index.js:97
Category: Logic Error

Problem:
When the leave/decline action is confirmed, the button is disabled and setAttribute("value", "leaving...") is called. But for a <button> element, the value attribute does not change the displayed text — only textContent or innerText does. The user will see "I Am Ready To Leave This Project" even while the action is processing.

Current Code:

leaveBtn.setAttribute("disabled", "disabled")
leaveBtn.setAttribute("value", "leaving...")

Suggested Fix:

leaveBtn.setAttribute("disabled", "disabled")
leaveBtn.textContent = "leaving..."

Issue 3: confirm-area display not reset on dismiss

File: components/gui/confirm/Confirm.js:43
Category: Logic Error

Problem:
In show(), this.closest(".confirm-area").style.display = "grid" is set inline. In dismiss(), only the "show" class is removed and the element itself is removed after 500ms. But the inline style.display = "grid" on the .confirm-area section is never cleared. After all confirms are dismissed, the .confirm-area section retains display: grid and height: 0vh (from CSS), which creates an invisible overlay that could interfere with pointer events in some browsers.

Suggested Fix:
In ConfirmContainer's positive/negative handlers (lines 25-36), after dismiss() and the dialog is gone, reset the section's display:

const positiveHandler = ({ detail }) => {
    if (this.#activeConfirmId && detail?.confirmId !== this.#activeConfirmId) return
    this.#confirmElem?.dismiss()
    this.#confirmElem = null
    this.#activeConfirmId = null
    // Reset display after animation
    setTimeout(() => {
        this.#screenLockingSection.style.display = ''
    }, 500)
}

How to Verify:

  1. Open and close a confirm dialog
  2. Inspect the .confirm-area element and verify display is not stuck on grid
  3. Verify you can click elements behind where the overlay was

Issue 4: Keyboard trap — no Escape key handling for confirm dialog

File: components/gui/confirm/ConfirmContainer.js
Category: Accessibility

Problem:
The confirm dialog takes over the screen with a full-viewport overlay but provides no way to dismiss via the Escape key. Users who rely on keyboard navigation have no way to cancel except clicking the negative button. The alert component has a similar gap but is less impactful since it only has one action.

Suggested Fix:
Add a keydown listener for Escape that triggers the negative action:

// In addConfirm(), after confirmButton.focus():
const escHandler = (e) => {
    if (e.key === 'Escape') {
        handleNegative()
        document.removeEventListener('keydown', escHandler)
    }
}
document.addEventListener('keydown', escHandler)

Issue 5: confirmAction import path uses relative ../api/events.js

File: utilities/confirmAction.js:1
Category: Maintainability

Problem:
The import from '../api/events.js' works only when the file is at the current depth. Other files in the project use ../../api/events.js or similar. If confirmAction.js were ever moved, this relative path would break. This is a minor concern given the current structure, but it's worth noting for consistency.


Suggestions 🔵

Suggestion 1: Consider a Promise-based API for confirmAction

Category: API Design

The callback-based confirmAction API works well for fire-and-forget patterns, but several call sites (e.g., line-parser.js:719-722, manage-project/index.js:51-71) would benefit from an await-able version. A Promise wrapper would enable cleaner async flows:

export function confirmActionAsync(message, options = {}) {
    return new Promise((resolve) => {
        confirmAction(message, () => resolve(true), () => resolve(false), options)
    })
}

This would allow:

if (await confirmActionAsync("Stop identifying lines?")) {
    location.href = ...
}

Suggestion 2: Focus trap within confirm dialog

Category: Accessibility

Currently confirmButton.focus() is called on show (good), but Tab can move focus outside the dialog to elements behind the overlay. Consider implementing a simple focus trap that cycles Tab between the two buttons while the dialog is active.


Suggestion 3: Consolidate button text defaults

Category: Consistency

The confirmAction utility and ConfirmContainer.addConfirm both default to "Yes" / "No" for button text. Most callers override these with more specific labels like "Delete" / "Cancel" or "Leave" / "Cancel". Consider whether the defaults should be "Confirm" / "Cancel" instead, as "Yes" / "No" is less informative for screen reader users who may not hear the full question.


Suggestion 4: Remove the async wrapper removal in line-parser deleteAllBtn handler

Category: Correctness

The change from async (e) => await this.deleteAllAnnotations(e) to (e) => this.deleteAllAnnotations(e) at line 470 is functionally equivalent since deleteAllAnnotations is no longer async (it uses confirmAction callbacks internally). However, the method still calls this.saveAnnotations() which returns a Promise chain. If error handling at the call site is ever needed, the non-async wrapper would swallow unhandled rejections silently. This is fine given the current internal .catch() handling but worth noting.


Files Reviewed

  • api/Project.js - Single alert()eventDispatcher.dispatch('tpen-alert') change
  • components/annotorious-annotator/line-parser.js - confirm()confirmAction() conversion
  • components/annotorious-annotator/plain.js - confirm()confirmAction() conversion
  • components/decline-project/index.js - Full refactor to use confirmAction()
  • components/gui/alert/Alert.js - Removed unused imports
  • components/gui/confirm/Confirm.js - Removed unused imports
  • components/gui/confirm/ConfirmContainer.js - Major enhancements (correlation IDs, stacking prevention, focus)
  • components/leave-project/index.js - Full refactor to use confirmAction()
  • components/manage-layers/index.js - confirm()confirmAction() conversion
  • components/manage-pages/index.js - confirm()confirmAction() conversion
  • components/manifest-import/index.js - confirm()confirmAction() conversion
  • components/navigation-manager/index.js - confirm()confirmAction() conversion
  • components/projects/index.js - alert()eventDispatcher.dispatch('tpen-alert') change
  • components/projects/project-list-write.js - alert()eventDispatcher.dispatch('tpen-alert') change
  • components/quicktype-manager/index.js - confirm()confirmAction() conversion
  • components/roles-handler/index.js - Full refactor with multiple confirmAction() usages
  • interfaces/manage-project/index.js - confirm()confirmAction() conversion
  • utilities/confirmAction.js - New file — core utility for the confirm dialog API

Checklist for Developer

  • Address all Critical issues (XSS in decline-project and leave-project)
  • Address all Major issues (malformed HTML, invalid ARIA role, listener leaks, null element binding)
  • Review and address Minor issues as appropriate
  • Consider Suggestions for future improvements
  • Run full test suite after changes
  • Self-review the fixes before requesting re-review

@cubap
Copy link
Copy Markdown
Member

cubap commented Mar 4, 2026

@copilot, have all these changes been addressed?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot, have all these changes been addressed?

Yes, all the issues raised in the review have been addressed. Here's a quick status summary:

Issue Status Commit
Race condition (stale listeners fire on wrong dialog) ✅ Fixed — confirmAction utility generates a confirmId per dialog; listeners guard on matching ID cfce025
Button mashing / stacked dialogs ✅ Fixed — ConfirmContainer blocks new dialogs while one is active; queues the next if needed cfce025
Keyboard accessibility (Tab/Enter/focus) ✅ Fixed — positive button auto-focused on open; Tab cycles between buttons; Enter activates; :focus-visible CSS added cfce025
"Yes/No" doesn't always fit the wording ✅ Fixed — all 14 call sites now pass contextual button labels (Delete/Cancel, Leave/Cancel, Publish/Cancel, Transfer/Cancel, Remove/Cancel, Discard/Keep Editing, etc.) cfce025
"Click 'OK' to continue?" in roles-handler ✅ Fixed — updated to "Do you want to continue?" cfce025
Alert fires before redirect in manage-project ✅ Fixed — redirect deferred via tpen-alert-acknowledged cfce025
No remaining raw alert()/confirm() calls ✅ Verified — grep finds none

Copilot AI requested a review from cubap March 4, 2026 19:30
Copilot finished work on behalf of cubap March 4, 2026 19:30
cubap added 4 commits March 4, 2026 13:59
Convert various modal/overlay containers to use the native <dialog> element and its showModal()/close() API. Update Alert and Confirm components to call showModal when showing and to toggle classes safely. Replace section/div overlays with dialog elements in AlertContainer, ConfirmContainer, ProjectTools, QuickTypeEditorDialog, and UpdateMetadata; add backdrop styles, sizing, and [open] display rules. Add cancel and backdrop click handlers to close dialogs (and prevent default cancel behavior). Ensure screen-locking sections are closed/cleaned up after dialogs dismiss (including retry logic in AlertContainer and delayed close in ConfirmContainer). Misc CSS tweaks to support full-viewport dialogs and backdrop behavior.
Introduce utilities/modalHost.js (openModalHost, closeModalHost, closeModalHostWhenEmpty) to centralize dialog host open/close logic and timing retries. Replace scattered showModal/close logic in Alert.js and Confirm.js to use openModalHost; update AlertContainer.js and ConfirmContainer.js to use closeModalHostWhenEmpty for coordinated closing after animations. Add addCustomAlert and a shared keyboard handler in AlertContainer, and simplify transcription code to call addCustomAlert directly. Files changed: components/gui/alert/Alert.js, components/gui/alert/AlertContainer.js, components/gui/confirm/Confirm.js, components/gui/confirm/ConfirmContainer.js, interfaces/transcription/index.js, utilities/modalHost.js.
@cubap cubap requested a review from thehabes March 4, 2026 20:29
@thehabes
Copy link
Copy Markdown
Member

thehabes commented Mar 4, 2026

Ok real world scenario AFK at this time.

@static-reviewer review this and I will take a closer look when I am back at the keyboard

@habesoftware-claude-connector
Copy link
Copy Markdown

Static Review Comments

Branch: copilot/refactor-alert-confirm-dialogs
Review Date: 2026-03-04
Reviewer: Static Reviewer - @thehabes

Bryan and Claude make mistakes. Verify all issues and suggestions. Avoid unnecessary scope creep.

Category Issues Found
🔴 Critical 3
🟠 Major 6
🟡 Minor 6
🔵 Suggestions 3

Critical Issues 🔴

Issue 1: Duplicate import of confirmAction — one from a module that does not export it

File: components/annotorious-annotator/line-parser.js:21
Category: Syntax / Runtime Error

Problem:
There are two imports of confirmAction: line 21 imports it from ../../api/events.js and line 24 imports it from ../../utilities/confirmAction.js. The api/events.js module does NOT export confirmAction. This will cause a runtime error or silently bind confirmAction to undefined before being overwritten by the second import. In strict module environments this is a parse-time error due to duplicate identifier declaration.

Current Code:

import { confirmAction } from '../../api/events.js'
import vault from '../../js/vault.js'
import '../page-selector/index.js'
import { confirmAction } from '../../utilities/confirmAction.js'

Suggested Fix:

import vault from '../../js/vault.js'
import '../page-selector/index.js'
import { confirmAction } from '../../utilities/confirmAction.js'

How to Verify:

  1. Remove the line import { confirmAction } from '../../api/events.js'
  2. Open the annotator page and verify no console errors on load
  3. Test the "Go Transcribe" button, "Delete annotation" click, and "Delete All Annotations" to confirm confirmAction works

Issue 2: Escape key dismisses confirm dialog without dispatching negative event — leaks event listeners

File: components/gui/confirm/ConfirmContainer.js:131
Category: Logic Error / Memory Leak

Problem:
When the user presses Escape, #handleKeydown calls this.dismissCurrent(), which only calls current.elem.dismiss() and dequeues. It does NOT dispatch tpen-confirm-negative. This means the confirmAction() utility's onPositive/onNegative listeners registered on the eventDispatcher are never cleaned up (they filter by confirmId and never match any dispatched event). Over time, this leaks listeners. Additionally, the onCancel callback from confirmAction() is never called, so any cancel-side-effect (like showing "Import canceled" in manifest-import) is silently skipped.

Current Code:

if (e.key === 'Escape') {
    e.preventDefault()
    this.dismissCurrent()
    return
}

Suggested Fix:

if (e.key === 'Escape') {
    e.preventDefault()
    // Trigger the negative/cancel button so that confirmAction listeners are cleaned up
    const denyButton = this.#confirmElem?.querySelector('.button-container button:last-child')
    if (denyButton) {
        denyButton.click()
    } else {
        this.dismissCurrent()
    }
    return
}

How to Verify:

  1. Open any confirm dialog (e.g., delete annotation, leave project)
  2. Press Escape key
  3. Verify the onCancel callback fires (e.g., manifest-import shows "Import canceled" message)
  4. Open and Escape multiple confirm dialogs and monitor the eventDispatcher for listener accumulation

Issue 3: Queue contradicts early-return guard — dialogs silently dropped, listeners orphaned

File: components/gui/confirm/ConfirmContainer.js:57
Category: Logic Error / Memory Leak

Problem:
Line 58 does if (this.#screenLockingSection.querySelector('tpen-confirm')) return which rejects any new confirm request if one is already showing. But the code also implements a #dialogQueue with #showCurrent() and dequeuing in dismissCurrent(). The early return means dialogs are silently dropped instead of queued. Combined with confirmAction() which has already registered event listeners for the dropped dialog's confirmId, those listeners will never fire and never be cleaned up — they become permanently orphaned.

Current Code:

addConfirm(message, positiveButtonText, negativeButtonText, confirmId) {
    if (!message || typeof message !== 'string') return
    // Prevent multiple dialogs from stacking when actions are triggered rapidly
    if (this.#screenLockingSection.querySelector('tpen-confirm')) return

Suggested Fix:
Either remove the early return to allow proper queuing:

addConfirm(message, positiveButtonText, negativeButtonText, confirmId) {
    if (!message || typeof message !== 'string') return
    // Removed early return — let the queue mechanism handle one-at-a-time display

Or keep the guard but dispatch a negative event for the dropped dialog to clean up listeners:

addConfirm(message, positiveButtonText, negativeButtonText, confirmId) {
    if (!message || typeof message !== 'string') return
    if (this.#screenLockingSection.querySelector('tpen-confirm')) {
        // Clean up listeners for the dropped dialog
        eventDispatcher.dispatch('tpen-confirm-negative', { confirmId })
        return
    }

How to Verify:

  1. Rapidly click two different confirm-triggering buttons
  2. Verify the second dialog either shows after the first is dismissed, or that its callbacks are properly cleaned up
  3. Monitor the eventDispatcher for orphaned listeners

Major Issues 🟠

Issue 4: CSS custom properties self-reference creates invalid declarations

File: components/gui/modal.css.js:10
Category: Styling Bug

Problem:
The :host block declares --primary-color: var(--primary-color, hsl(186, 84%, 40%)). Per the CSS spec, when a custom property references itself (even with a fallback), the property becomes invalid at computed-value time and resolves to the initial value (effectively unset). The fallback value hsl(186, 84%, 40%) will NOT be used. This means all buttons and elements depending on --primary-color, --primary-light, etc. will lose their intended styling. All seven custom properties in this block have the same issue.

Current Code:

:host {
    --primary-color: var(--primary-color, hsl(186, 84%, 40%));
    --primary-light: var(--primary-light, hsl(186, 84%, 60%));
    --light-color  : var(--light-color, hsl(186, 84%, 90%));
    --dark         : var(--dark, #2d2d2d);
    --white        : var(--white, hsl(0, 0%, 100%));
    --gray         : var(--gray, hsl(0, 0%, 60%));
    --light-gray   : var(--light-gray, hsl(0, 0%, 90%));
}

Suggested Fix:

:host {
    --primary-color: hsl(186, 84%, 40%);
    --primary-light: hsl(186, 84%, 60%);
    --light-color  : hsl(186, 84%, 90%);
    --dark         : #2d2d2d;
    --white        : hsl(0, 0%, 100%);
    --gray         : hsl(0, 0%, 60%);
    --light-gray   : hsl(0, 0%, 90%);
}

How to Verify:

  1. Open a page that triggers an alert or confirm dialog
  2. Inspect the dialog buttons and verify they display the teal/primary color styling
  3. If the intent is to allow parent-context overrides, apply fallbacks only at usage sites: color: var(--primary-color, hsl(186, 84%, 40%))

Issue 5: role="confirm" is not a valid ARIA role

File: components/gui/confirm/Confirm.js:18 and components/gui/modal.css.js:55
Category: Accessibility

Problem:
The Confirm component sets <output role="confirm"> in its shadow DOM, and modal.css.js uses [role="confirm"] selectors (lines 55, 73, 80, 88, 105). However, confirm is NOT a valid ARIA role. Screen readers will either ignore it or behave unpredictably. Some browsers may strip invalid roles entirely, causing the CSS selector [role="confirm"] to not match — which would break the confirm dialog's visual styling.

Current Code:

<output role="confirm">
  <slot></slot>
</output>

Suggested Fix:

<output role="alertdialog">
  <slot></slot>
</output>

Update all [role="confirm"] selectors in modal.css.js to [role="alertdialog"].

How to Verify:

  1. Change role="confirm" to role="alertdialog" in Confirm.js
  2. Update all [role="confirm"] selectors in modal.css.js to [role="alertdialog"]
  3. Visually verify the confirm dialog renders with correct styles
  4. Test with a screen reader to verify proper announcement

Issue 6: Shadow DOM activeElement mismatch breaks keyboard navigation

File: components/gui/confirm/ConfirmContainer.js:139
Category: Logic Error

Problem:
The #handleKeydown method checks document.activeElement?.tagName === 'BUTTON' for Enter key handling (line 139) and compares document.activeElement against buttons for Tab handling (lines 149–150). However, the confirm dialog buttons live inside the tpen-confirm element's shadow DOM. When focus is on a button inside shadow DOM, document.activeElement points to the shadow host (tpen-confirm), not the actual button. This means Enter does nothing and Tab focus trapping fails.

Current Code:

if (e.key === 'Enter') {
    e.preventDefault()
    if (document.activeElement?.tagName === 'BUTTON') {
        document.activeElement.click()
    }
    return
}

Suggested Fix:

if (e.key === 'Enter') {
    e.preventDefault()
    // Traverse shadow DOM to find the actual focused element
    let focused = document.activeElement
    while (focused?.shadowRoot?.activeElement) {
        focused = focused.shadowRoot.activeElement
    }
    if (focused?.tagName === 'BUTTON') {
        focused.click()
    }
    return
}

Apply the same shadow DOM traversal for the Tab key handler.

How to Verify:

  1. Open a confirm dialog
  2. Tab to the positive button and press Enter — verify the positive action fires
  3. Tab to the negative button and press Enter — verify the negative action fires
  4. Verify Tab and Shift+Tab cycle between the two buttons correctly

Issue 7: Positive/negative handlers dismiss without verifying confirmId

File: components/gui/confirm/ConfirmContainer.js:34
Category: Logic Error

Problem:
The positiveHandler and negativeHandler in connectedCallback() call this.dismissCurrent() unconditionally without checking the confirmId from the event detail matches the currently displayed dialog. Since confirmAction() dispatches events with a specific confirmId, a mismatched event could dismiss the wrong dialog.

Current Code:

const positiveHandler = () => this.dismissCurrent()
const negativeHandler = () => this.dismissCurrent()

Suggested Fix:

const positiveHandler = (ev) => {
    const current = this.#dialogQueue[0]
    if (current && ev.detail?.confirmId === current.confirmId) {
        this.dismissCurrent()
    }
}

How to Verify:

  1. Store confirmId on each dialog queue entry
  2. Verify that dismiss only fires when the confirmId matches

Issue 8: Global Enter key capture in AlertContainer breaks other page inputs

File: components/gui/alert/AlertContainer.js:88
Category: UX / Logic Error

Problem:
The #attachKeyboardHandler intercepts both Escape AND Enter keys globally on document with e.preventDefault(). While an alert is open, this prevents Enter from working in any other input, textarea, or form on the page. The handler should scope the Enter behavior to the alert context.

Current Code:

this.#keydownHandler = (e) => {
    if (e.key === 'Escape' || e.key === 'Enter') {
        e.preventDefault()
        this.#dismissAlert(alertElem)
    }
}

Suggested Fix:

this.#keydownHandler = (e) => {
    if (e.key === 'Escape') {
        e.preventDefault()
        this.#dismissAlert(alertElem)
    }
    if (e.key === 'Enter' && (e.target === alertElem || alertElem.contains(e.target))) {
        e.preventDefault()
        this.#dismissAlert(alertElem)
    }
}

How to Verify:

  1. Display an alert dialog
  2. While it is visible, attempt to type in another input on the page
  3. Press Enter — verify it does not interfere with other inputs
  4. Click the OK button or press Escape — verify the alert dismisses

Issue 9: Unscoped tpen-alert-acknowledged listener could fire from wrong alert

File: interfaces/manage-project/index.js:78
Category: Logic Error

Problem:
The code dispatches tpen-alert and then listens for tpen-alert-acknowledged with .one() to trigger a redirect. Since tpen-alert-acknowledged is not scoped to a specific alert instance, if a different alert was already showing and gets dismissed first, the .one() listener would fire and redirect the user incorrectly.

Current Code:

TPEN.eventDispatcher.dispatch('tpen-alert', { message: "You do not have permissions to use this page." })
TPEN.eventDispatcher.one('tpen-alert-acknowledged', () => {
    document.location.href = `/project?projectID=${TPEN.screen.projectInQuery}`
})
return

Suggested Fix:
Consider adding an alertId mechanism similar to confirmId, or use confirmAction which already has scoped IDs:

import { confirmAction } from '../../utilities/confirmAction.js'

confirmAction(
    "You do not have permissions to use this page.",
    () => { document.location.href = `/project?projectID=${TPEN.screen.projectInQuery}` },
    () => { document.location.href = `/project?projectID=${TPEN.screen.projectInQuery}` },
    { positiveButtonText: "OK" }
)

How to Verify:

  1. Navigate to manage-project as a user without edit permissions
  2. Verify the alert shows and clicking OK redirects
  3. Attempt to trigger multiple alerts simultaneously and verify only the permission alert redirect fires

Minor Issues 🟡

Issue 10: @media (prefers-reduced-motion) should be (prefers-reduced-motion: reduce)

File: components/gui/modal.css.js:86
Category: Accessibility / CSS

Problem:
@media (prefers-reduced-motion) evaluates to true when the feature is supported by the browser, regardless of the user's actual preference. The correct syntax is (prefers-reduced-motion: reduce).

Current Code:

@media (prefers-reduced-motion) {

Suggested Fix:

@media (prefers-reduced-motion: reduce) {

How to Verify:

  1. Enable "Reduce motion" in OS accessibility settings
  2. Verify dialogs appear immediately without animation
  3. Disable the setting and verify animations play normally

Issue 11: Default focus on destructive button violates UX best practice

File: components/gui/confirm/ConfirmContainer.js:112
Category: UX

Problem:
#showCurrent() focuses the positive button (e.g., "Delete", "Remove", "Transfer") by default. For destructive actions, UX best practice is to focus the non-destructive/cancel button to prevent accidental Enter key presses from triggering the destructive action.

Suggested Fix:

// Focus the non-destructive (negative/cancel) button by default
current.buttons.negative.focus()

Issue 12: Tab key logic is overly complex with confusing fallback

File: components/gui/confirm/ConfirmContainer.js:155
Category: Code Quality

Problem:
The Shift+Tab/Tab logic uses if (isPositive || !isNegative) which is confusing. For a two-button cycle, a simpler approach improves readability.

Suggested Fix:

if (e.shiftKey) {
    e.preventDefault()
    const target = isNegative ? buttons[0] : buttons[1]
    target.focus()
} else {
    e.preventDefault()
    const target = isPositive ? buttons[1] : buttons[0]
    target.focus()
}

Issue 13: Multiple alerts can stack but keyboard handler only tracks the last one

File: components/gui/alert/AlertContainer.js:100
Category: Logic Error

Problem:
Each call to addAlert() replaces this.#keydownHandler with a new handler tied to the newest alert. If a previous alert is still showing (animating out), Escape/Enter will only dismiss the newest alert, potentially leaving previous alerts stuck.


Issue 14: Custom keyboard handler conflicts with native <dialog> Escape handling

File: components/gui/confirm/ConfirmContainer.js:117
Category: Code Quality

Problem:
When using <dialog>.showModal(), the browser provides native Escape key handling. Adding a separate document.addEventListener('keydown') for Escape can conflict — the native handler may fire first, closing the dialog before the custom handler runs, or vice versa. Consider listening for the cancel event on the dialog element instead.


Issue 15: Dialog min-width/max-width uses vw units — breaks on mobile

File: components/gui/modal.css.js:65
Category: Responsive Design

Problem:
min-width: 25vw; max-width: 35vw results in dialogs only ~94px to ~131px wide on 375px mobile viewports.

Suggested Fix:

min-width: min(25vw, 300px);
max-width: min(35vw, 500px);

@media (max-width: 768px) {
    min-width: 80vw;
    max-width: 90vw;
}

Suggestions 🔵

Suggestion 16: No safety net for orphaned confirmAction listeners

File: utilities/confirmAction.js:16

If confirmAction() is called but the tpen-confirm-container is not in the DOM, the dispatched event goes unhandled and the onPositive/onNegative listeners remain registered forever. Consider adding a safety timeout or verifying the container exists before dispatching.


Suggestion 17: Alert/Confirm containers placed outside <body>

File: components/gui/alert/AlertContainer.js:127 and components/gui/confirm/ConfirmContainer.js:216

document?.body.after(new AlertContainer()) places the element as a sibling after <body>, which is technically invalid HTML. Consider using document.body.appendChild() instead.


Suggestion 18: Double-darkened backdrop overlay

File: components/gui/modal.css.js:33

Both the dialog element (background-color: rgba(0, 0, 0, 0.7)) and its ::backdrop pseudo-element (background-color: rgba(0, 0, 0, 0.7)) have dark backgrounds. When showModal() is used, both render simultaneously, resulting in an effectively ~0.91 opacity dark overlay — nearly opaque. Set the dialog background to transparent and keep the backdrop styling, or vice versa.


Files Reviewed

  • api/Project.js - Minor event dispatch change, clean
  • components/annotorious-annotator/line-parser.js - Critical: duplicate import
  • components/annotorious-annotator/plain.js - Clean migration to confirmAction
  • components/decline-project/index.js - Clean migration to dialog-based confirm
  • components/gui/alert/Alert.js - Migrated to <dialog>, minor a11y gap
  • components/gui/alert/AlertContainer.js - Multiple keyboard/stacking issues
  • components/gui/confirm/Confirm.js - Invalid ARIA role="confirm"
  • components/gui/confirm/ConfirmContainer.js - Multiple critical/major issues
  • components/gui/modal.css.js - Self-referencing CSS vars, invalid role selectors, double backdrop
  • components/leave-project/index.js - Clean migration to confirmAction
  • components/manage-layers/index.js - Clean migration
  • components/manage-pages/index.js - Clean migration
  • components/manifest-import/index.js - Clean migration
  • components/navigation-manager/index.js - Clean migration
  • components/project-tools/index.js - Minor redundant preventDefault/close
  • components/projects/index.js - Trivial import path fix
  • components/projects/project-list-write.js - Trivial import path fix
  • components/quicktype-manager/index.js - Clean migration
  • components/quicktype-tool/quicktype-editor-dialog.js - Clean dialog migration
  • components/roles-handler/index.js - Clean migration to confirmAction
  • components/update-metadata/index.css - Style consolidation, clean
  • components/update-metadata/index.html - Minor structural cleanup
  • components/update-metadata/index.js - Clean migration
  • interfaces/manage-project/index.js - Unscoped alert listener issue
  • interfaces/transcription/index.js - Clean migration
  • utilities/confirmAction.js - New utility, works well but no safety net
  • utilities/modalHost.js - New utility, clean implementation

If there are significant code changes in response to this review please test those changes. Run the application manually and test or perform internal application tests when applicable.

Introduce alert queuing and improve modal/confirm behavior and styling. Alerts are now queued (only one shown at a time) with addAlert/addCustomAlert pushing entries to a queue; show/dismiss logic uses a #showCurrent/dismissCurrent flow, native dialog cancel events for Escape, and Enter handling for activation. Confirm dialogs now use role="alertdialog", default focus is set to the negative/cancel button, keyboard navigation is improved (Tab/Shift+Tab cycling, Enter activation through shadow DOM), and positive/negative handlers validate confirmId against the active dialog. Both containers now append their singleton to document.body, and cancel handlers are removed when dialogs are dismissed to avoid leaks. Shared modal CSS was updated (transparent backdrop, color variables, selector changes to alertdialog, button layout and responsive tweaks). Also fixed an import in line-parser to use the utilities confirmAction path.
@cubap
Copy link
Copy Markdown
Member

cubap commented Mar 5, 2026

@thehabes Feel free to poke at this and see if you can find anything broken.

Copy link
Copy Markdown
Member

@cubap cubap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some styling is still crunchy, but I think the layering, tabbing, dismissing etc all works

Copy link
Copy Markdown
Member

@thehabes thehabes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated the same test as the first review and could not reproduce any reported error.

Further tested by causing both success and error scenarios behind the positive selection. Errors occurred and were reported to the user correctly, and the dialogues dismiss in both scenarios.

No noticeable gaps in "keyboard only" functionality. Tab selection, selection indicators, focus, esc, and enter all work as expected.

The dialogues were never blocked by other element and were always actionable.

Static Review and Copilot Review also seem happy with it.

@cubap cubap merged commit db0eceb into main Mar 5, 2026
3 checks passed
@cubap cubap deleted the copilot/refactor-alert-confirm-dialogs branch March 5, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

alert() and confirm() where a dialog or modal makes more sense

4 participants