Skip to content

Dialog migration: Angular Material dialogs to m3e-angular

What this is: the delivery plan for #4599: every SyRF dialog moves from Angular Material's MatDialog to m3e-angular's Material 3 Expressive MatExpDialog (ADR-027). Owner direction (2026-10-27): "all dialog components in syrf should be replaced with the m3e-angular version". This page is temporary; delete it when the programme ends.

Design

m3e-angular 0.1.0-alpha.7 opens a component with MatExpDialog.open(component, { mode, title, ... }), where mode is 'adaptive' (tasks and forms: close button, full screen below 600px) or 'alert' (short confirmation, needs role and ariaDescribedBy). Content uses <mat-exp-dialog-layout>, <ng-template matExpDialogActions>, [matExpDialogClose] and MatExpDialogRef, which is not type-identical to MatDialogRef. MAT_DIALOG_DATA works under both.

SyRF wraps that in src/services/web/src/app/shared/dialog/:

Piece Role
SyrfDialogService The one opener. Reads expressiveDialogs once, at open: on, it calls MatExpDialog.open; off, it opens the same component with MatDialog, as before. Adaptive dialogs drop per-dialog sizes under m3e, which owns that geometry. Adds the panel class syrf-dialog-panel in both modes (an E2E hook). getDialogById searches both openers.
<syrf-dialog-layout> + <ng-template syrfDialogActions> The frame: <mat-exp-dialog-layout> under m3e; the native title, content and actions under MatDialog. The heading is always the opener's title.
[syrfDialogClose] Closes with a result under either opener (a bare marker closes with undefined; type="button" by default).
injectSyrfDialogRef() The content's dialog reference from whichever opener opened it (SyrfDialogRef: close, afterClosed, beforeClosed, afterOpened, backdropClick, keydownEvents, disableClose, getState).

Because content renders under either opener, the flag lives in one place, and switching it never affects an open dialog.

Regression guard: shared/dialog/no-legacy-mat-dialog.spec.ts fails when a file outside legacy-mat-dialog.allowlist.json uses MatDialog, MatDialogRef, the native dialog directives or Material's dialog classes in styles. The allow-list records each remaining file's wave and only shrinks (the guard also fails when a listed file is already migrated). The two shared files keep their native branch, as justified exceptions, until the flag retires.

Migrating one dialog

  1. Template: wrap the body in <syrf-dialog-layout> and move the buttons into <ng-template syrfDialogActions>. Delete the native title element; the opener's title is the heading. Replace fxLayout on the old content element with component SCSS.
  2. Close: [mat-dialog-close]="x" becomes [syrfDialogClose]="x", and an injected MatDialogRef becomes injectSyrfDialogRef(). Keep every result value exactly as it was. A bare mat-dialog-close closed with '', but a bare syrfDialogClose closes with undefined (like m3e and Escape). Check the openers: if none tells '' from undefined (most only test truthiness), use the bare marker; otherwise bind [syrfDialogClose]="''".
  3. Opener: inject SyrfDialogService; pass mode ('adaptive' unless it is a short alert) and the title the template used to show (export it as a constant next to the component when it is static). Keep data, disableClose, autoFocus, restoreFocus and id as they were.
  4. Specs: content specs provide MatDialogRef (or open through the real service with the flag both ways); opener specs mock SyrfDialogService and assert the component, mode and title.
  5. Remove the migrated files from the allow-list; update any E2E page object that targets mat-dialog-container to .syrf-dialog-panel (both modes).

Waves

One PR per wave, each ready for review, merged in order. Each wave lands with the flag off everywhere.

Wave Scope PR Notes
W0 Shared layer, flag, guard; DeleteModalComponent, ErrorDialogComponent and their openers #4619
W1 Rest of shared/dialogs (confirmation, info confirmation, access denied, permissions, stage overview, surplus notification, version info) and every opener of them #4629 Confirmation and info confirmation have about 22 opener files
W2 core (environment warning, PR description, error recovery, version check, signed out, unauthenticated)
W3 project/project-overview, project/project-admin After #4608 (systematic searches, create stage) and #4574 (bulk PDF dialog) merge
W4 stage, shared/annotation, screening, studies AF2 rules apply (.claude/rules/web-af2.md)
W5 project/data-export, project-index, manage, admin, shared/beta-features
W6 Allow-list empty (or justified exceptions); flag settles and the native branch is deleted Owner decides when the flag switches on

Acceptance criteria

# Criterion Verification
AC1 With expressiveDialogs off, every migrated dialog opens through MatDialog with the same title, content, actions, results, dismissal and focus behaviour as before Content and opener specs per wave; the flag-off cases in syrf-dialog.service.spec.ts
AC2 With it on, every migrated dialog opens through MatExpDialog: Expressive surface, close button, full screen below 600px, Escape and backdrop dismissal unchanged Flag-on specs; preview screenshots at 1440px and 390px per wave
AC3 Switching on, off and on is safe; an open dialog keeps the presentation it opened with Round-trip spec in syrf-dialog.service.spec.ts
AC4 Result values are unchanged for every dialog (including undefined on dismissal) Per-dialog specs asserting afterClosed() values
AC5 No file uses native dialog APIs outside the allow-list and its justified exceptions; the allow-list only shrinks no-legacy-mat-dialog.spec.ts
AC6 E2E selectors work in both modes Page objects target .syrf-dialog-panel; E2E smoke on the wave touching them
AC7 Programme complete: allow-list empty apart from justified exceptions Guard spec on main

Open questions and library gaps

  • Flag (DM-1). Applied: one default-off flag, expressiveDialogs, read only by SyrfDialogService. The repo rules call for a kill switch on a programme of visible changes built on a pre-release library, and require main to stay promotable while waves land. This was not an owner answer. Dropping the flag later is a small change in one file.
  • Wide dialogs. m3e's adaptive dialog is 560px wide (the stepper is 880px). Dialogs that need more (tables, image cropper, graph tools) are assessed in their wave; a missing width option is a library gap, fixed in m3e-angular rather than worked around here.
  • Plain confirmations. MatExpDialog.confirm() takes plain text only. SyRF's confirmation dialogs take HTML and a third button, so W1 keeps them as components on <syrf-dialog-layout>.

Inventory

On main at ac4d774339 (2026-10-27), 148 non-spec files referenced native dialog APIs: about 50 dialog content components and about 55 opener files. The live list, with each file's wave, is legacy-mat-dialog.allowlist.json.