import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; /// 다이얼로그에서 ESC/Enter 키를 처리하고 포커스를 트랩하는 래퍼 위젯. class DialogKeyboardShortcuts extends StatefulWidget { const DialogKeyboardShortcuts({ super.key, required this.child, this.onEscape, this.onSubmit, this.enableFocusTrap = true, }); final Widget child; final VoidCallback? onEscape; final FutureOr Function()? onSubmit; final bool enableFocusTrap; @override State createState() => _DialogKeyboardShortcutsState(); } class _DialogKeyboardShortcutsState extends State { late final FocusScopeNode _focusScopeNode; @override void initState() { super.initState(); _focusScopeNode = FocusScopeNode(debugLabel: 'DialogKeyboardShortcuts'); } @override void dispose() { _focusScopeNode.dispose(); super.dispose(); } bool get _hasSubmitHandler => widget.onSubmit != null; bool _shouldHandleSubmitKey() { if (!_hasSubmitHandler) { return false; } final primaryFocus = FocusManager.instance.primaryFocus; if (primaryFocus == null) { return true; } final context = primaryFocus.context; if (context == null) { return true; } EditableText? editable; final widget = context.widget; if (widget is EditableText) { editable = widget; } else { editable = context.findAncestorWidgetOfExactType(); } if (editable != null) { // Multi-line 입력에서는 엔터 키를 입력값으로 전달한다. final bool isMultiline = editable.maxLines == null || editable.maxLines! > 1; if (isMultiline) { return false; } } return true; } Map get _shortcuts { final shortcuts = { LogicalKeySet(LogicalKeyboardKey.escape): const _DismissIntent(), }; if (_hasSubmitHandler) { shortcuts[LogicalKeySet(LogicalKeyboardKey.enter)] = const _SubmitIntent(); shortcuts[LogicalKeySet(LogicalKeyboardKey.numpadEnter)] = const _SubmitIntent(); } return shortcuts; } Map> get _actions { return >{ _DismissIntent: CallbackAction<_DismissIntent>( onInvoke: (intent) { widget.onEscape?.call(); return null; }, ), _SubmitIntent: CallbackAction<_SubmitIntent>( onInvoke: (intent) { if (_shouldHandleSubmitKey()) { final callback = widget.onSubmit; if (callback != null) { final result = callback(); if (result is Future) { return result; } } } return null; }, ), }; } @override Widget build(BuildContext context) { Widget content = widget.child; if (widget.enableFocusTrap) { content = FocusTraversalGroup( policy: WidgetOrderTraversalPolicy(), child: FocusScope( node: _focusScopeNode, autofocus: true, child: content, ), ); } else { content = FocusTraversalGroup( policy: WidgetOrderTraversalPolicy(), child: FocusScope(autofocus: true, child: content), ); } return Shortcuts( shortcuts: _shortcuts, child: Actions(actions: _actions, child: content), ); } } class _DismissIntent extends Intent { const _DismissIntent(); } class _SubmitIntent extends Intent { const _SubmitIntent(); }