You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

三星Galaxy Fold 5打开Angular Material Dialog报错排查

三星Galaxy Fold 5上Angular Material Dialog无法显示的问题排查

今年夏天将企业Web应用从Angular 6升级到Angular 15后,核心用户反馈:在三星Galaxy Fold 5手机上打开特定Angular Material Dialog时,对话框无法显示,报错如下:

TypeError: Cannot read properties of undefined (reading 'focusInitialElementWhenReady')
at bt._trapFocus (vendor.68845b091518ea76.js:1:1028636)
at bt._openAnimationDone (vendor.68845b091518ea76.js:1:1487405)
at bt._onAnimationDone (vendor.68845b091518ea76.js:1:1493261)
at vendor.68845b091518ea76.js:1:1494469
at Se (vendor.68845b091518ea76.js:1:1265844)
at b (vendor.68845b091518ea76.js:1:1266012)
at vendor.68845b091518ea76.js:1:1828893
at z.invoke (polyfills.729dcc77ee327c3e.js:1:6654)
at Object.onInvoke (vendor.68845b091518ea76.js:1:1296471)
at z.invoke (polyfills.729dcc77ee327c3e.js:1:6594)

该问题仅在三星Galaxy Fold 5上出现,与浏览器无关,且无法在其他设备或模拟器中复现。

对话框组件代码

export class DocumentPropertiesDialogComponent {
    smShowCheckBoxList: boolean = false;
    viewModelSubject$: ViewModelSubject<DocumentMetaDataViewModel> = new ViewModelSubject<DocumentMetaDataViewModel>();
    readonly documentRights$: Observable<DocumentRightsDTO>;

    static show(dh: DialogHelper, document: DocumentDTO): Observable<MetaDataUpdateDialogResult> {
        return dh.showDialog<DocumentPropertiesDialogComponent, DocumentMetaDataDialogData, MetaDataUpdateDialogResult>
            (DocumentPropertiesDialogComponent, new DocumentMetaDataDialogData(document), {
                maxHeight: '95vh',
                maxWidth: '100vw',
                minHeight: '80vh',
                xsFullscreen: true,
            });
    }

    constructor(public dialogRef: MatDialogRef<DocumentPropertiesDialogComponent>,
        @Inject(MAT_DIALOG_DATA) data: DocumentMetaDataDialogData,
        public modelCache: ModelCache,
        public urlFactory: UrlFactory,
        private _documentMetaDataHelper: DocumentMetaDataHelper,
        public windowService: WindowService,
        private _dirtyMetaDataInputHelper: DirtyMetaDataInputHelper,
        private _documentFunctionService: DocumentFunctionService,
        private readonly documentService: DocumentService
    ) {
        const document = data.document;
        this.smShowCheckBoxList = !document.metaData || (!document.metaData.assignedDocumentClasses || document.metaData.assignedDocumentClasses.length === 0);
        document.initializeDocumentMetaDataHelper(this._documentMetaDataHelper);
        if (data.isUploadDialog && data.document) {
            data.document.hasChanges.next(true);
        }
        this.viewModelSubject$.next(DocumentMetaDataViewModel.createNew(this.modelCache, data.document, data.isUploadDialog, data.documentDepotTypeArchiveAssignments, data.depotTypeID));
        this.documentRights$ = this.documentService.getDocumentRights(document.documentID).pipe(
            shareReplay(1),
        );
    }

    ...
}

DialogHelper代码

@Injectable()
export class DialogHelper {
    private _dialogNumber: number = 0;

    constructor(public dialogFactory: MatDialog, private ngZone: NgZone, private windowService: WindowService) { }

    get dialogNumber(): number {
        return this._dialogNumber;
    }

    showDialog<T, D = any, R = any>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>, data?: D, props?: MatDialogConfig<D> | ShowDialogConfig<D>): Observable<R>;;
    showDialog<T, D = any, R = any>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
        data: D,
        hasBackdrop?: boolean | null,
        panelClass?: string | null,
        disableClose?: boolean | null,
        xsFullScreen?: boolean | null,
        autofocus?: boolean | null,
        matDialogConfig?: MatDialogConfig<D> | undefined | null,
    ): Observable<R>;;
    showDialog<T, D = any, R = any>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>, data: D,
        hasBackdropOrConfig: boolean | MatDialogConfig<D> | ShowDialogConfig<D> | null,
        panelClass: string | null,
        disableClose: boolean | null,
        xsFullScreen: boolean | null,
        autofocus: boolean | null,
        matDialogConfig: MatDialogConfig<D> | null): Observable<R>;;
    showDialog<T, D = any, R = any>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>, data: D = null,
        hasBackdropOrConfig: boolean | MatDialogConfig<D> | ShowDialogConfig<D> | null = true,
        panelClass: string | null = null,
        disableClose: boolean | null = false,
        xsFullScreen: boolean | null = false,
        autofocus: boolean | null = true,
        matDialogConfig: MatDialogConfig<D> | null = null): Observable<R> {
        if (hasBackdropOrConfig instanceof MatDialogConfig) {
            const config: MatDialogConfig<D> = hasBackdropOrConfig;
            config.data = data;
            config.closeOnNavigation = true;
            this._dialogNumber++;
            return this.ngZone.run(() => this.openDialog(componentOrTemplateRef, config));
        }
        else {
            const config = matDialogConfig ? matDialogConfig : new MatDialogConfig<D>();

            if (typeof hasBackdropOrConfig === 'boolean') {
                config.hasBackdrop = hasBackdropOrConfig;

                return this.showDialog<T, D, R>(componentOrTemplateRef, data, config,
                    panelClass, disableClose, xsFullScreen, autofocus, matDialogConfig);
            }
            else {
                Object.assign(config, hasBackdropOrConfig);
            }

            if (!this.windowService.size$.value.xsOrLess || xsFullScreen) {
                config.maxHeight = '100vh';
                config.maxWidth = '100vw';
            }

            return this.showDialog<T, D, R>(componentOrTemplateRef, data, config,
                panelClass, disableClose, xsFullScreen, autofocus, undefined);
        }
    }

    public isDialogOpen(): boolean {
        return this._dialogNumber > 0;
    }

    private openDialog<T, D = any, R = any>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
        config: MatDialogConfig<D>): Observable<R> {
        const self = this;
        return this.dialogFactory.open<T, D, R>(componentOrTemplateRef, config)
            .afterClosed()
            .pipe(finalize(() => {
                self._dialogNumber--;
            }));
    }
}

package.json核心依赖

"dependencies": {
    "@angular/animations": "15.2.9",
    "@angular/cdk": "15.2.9",
    "@angular/common": "15.2.9",
    "@angular/compiler": "15.2.9",
    "@angular/core": "15.2.9",
    "@angular/elements": "15.2.9",
    "@angular/forms": "15.2.9",
    "@angular/material": "15.2.9",
    "@angular/platform-browser": "15.2.9",
    "@angular/platform-browser-dynamic": "15.2.9",
    "@angular/router": "15.2.9",
    "@material-design-icons/font": "0.14.2",
    "@ngx-translate/core": "14.0.0",
    "hammerjs": "2.0.8",
    "ng-animate": "2.0.1",
    "ng-circle-progress": "1.5.1",
    "rxjs": "6.5.4",
    "tslib": "2.3.0",
    "zone.js": "0.11.4",
    ...
  },
错误原因分析与解决方案

可能原因

  • 折叠屏设备视图尺寸异常:三星Galaxy Fold 5的折叠/展开状态下,视图尺寸快速变化可能触发Angular CDK焦点陷阱逻辑异常,focusInitialElementWhenReady所属的焦点陷阱实例未正确初始化或在动画完成时已被销毁。
  • Angular Material 15焦点处理兼容性问题:Angular 15.x的Material组件在折叠屏这类特殊移动设备上存在焦点陷阱的bug,动画完成后实例绑定丢失。
  • 对话框配置冲突:xsFullscreen: true结合折叠屏的特殊尺寸,可能导致对话框DOM元素在动画阶段出现异常,干扰焦点陷阱初始化。

解决方案

  1. 禁用对话框自动焦点:在打开对话框的配置中添加autoFocus: false,绕过焦点陷阱的自动初始化逻辑,避免未定义实例调用:

    static show(dh: DialogHelper, document: DocumentDTO): Observable<MetaDataUpdateDialogResult> {
        return dh.showDialog<DocumentPropertiesDialogComponent, DocumentMetaDataDialogData, MetaDataUpdateDialogResult>
            (DocumentPropertiesDialogComponent, new DocumentMetaDataDialogData(document), {
                maxHeight: '95vh',
                maxWidth: '100vw',
                minHeight: '80vh',
                xsFullscreen: true,
                autoFocus: false // 添加此行
            });
    }
    
  2. 升级Angular CDK/Material版本:Angular 15.2.x后续小版本可能修复了折叠屏焦点处理bug,尝试升级到15.x系列的最新稳定版本:

    npm install @angular/cdk@15.2.10 @angular/material@15.2.10
    
  3. 手动初始化焦点陷阱(备选):如果禁用自动焦点后需要手动设置焦点,可在对话框组件的ngAfterViewInit钩子中手动创建并激活焦点陷阱:

    import { FocusTrapFactory } from '@angular/cdk/a11y';
    import { ElementRef, ViewChild, AfterViewInit } from '@angular/core';
    
    export class DocumentPropertiesDialogComponent implements AfterViewInit {
        @ViewChild('focusTarget') focusTarget: ElementRef;
    
        constructor(private focusTrapFactory: FocusTrapFactory) {}
    
        ngAfterViewInit() {
            const focusTrap = this.focusTrapFactory.create(this.focusTarget.nativeElement);
            focusTrap.focusInitialElement();
        }
    }
    

    同时在模板中添加标记元素:

    <div #focusTarget>...</div>
    
  4. 调整对话框尺寸配置:移除xsFullscreen: true,改用更温和的尺寸适配,避免极端尺寸导致DOM异常:

    {
        maxHeight: '95vh',
        maxWidth: '95vw',
        minHeight: '80vh',
        width: '90vw'
    }
    

内容的提问来源于stack exchange,提问作者Florian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 06:18:14