Repository: rrousselGit/flutter_hooks

Issue:
Add focusScopeNode hook
**Is your feature request related to a problem? Please describe.**
Currently working on some desktop and TV application with lot of focus controls to play. I use `useFocusNode` and totally love it, but lack the hook for focus scope node for the `FocusScopeWidget` really gets into the way creating one hook separately for the internal semi widely used widget's focus node. 

**Describe the solution you'd like**
Would like to have it in the package itself. Creating a new `useFocusScopeNode` hook. 

**Describe alternatives you've considered**
I am using a separate hook file in the projects for this.

**Additional context**
I'll be having a pull request for this coming. Would love to contribute!




Fix patch:
diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md
index 71b458c4..bccbfc0a 100644
--- a/packages/flutter_hooks/CHANGELOG.md
+++ b/packages/flutter_hooks/CHANGELOG.md
@@ -1,7 +1,7 @@
-## Unreleased docs
-
-Added korean translation thanks to @sejun2
+## Unreleased minor
 
+- Added korean translation (thanks to @sejun2)
+- Add `useFocusScopeNode` hook (thanks to @iamsahilsonawane)
 ## 0.18.5+1
 
 Update links to the repository
diff --git a/packages/flutter_hooks/lib/src/focus.dart b/packages/flutter_hooks/lib/src/focus_node.dart
similarity index 100%
rename from packages/flutter_hooks/lib/src/focus.dart
rename to packages/flutter_hooks/lib/src/focus_node.dart
diff --git a/packages/flutter_hooks/lib/src/focus_scope_node.dart b/packages/flutter_hooks/lib/src/focus_scope_node.dart
new file mode 100644
index 00000000..297a1bb0
--- /dev/null
+++ b/packages/flutter_hooks/lib/src/focus_scope_node.dart
@@ -0,0 +1,74 @@
+part of 'hooks.dart';
+
+/// Creates an automatically disposed [FocusScopeNode].
+///
+/// See also:
+/// - [FocusScopeNode]
+FocusScopeNode useFocusScopeNode({
+  String? debugLabel,
+  FocusOnKeyCallback? onKey,
+  FocusOnKeyEventCallback? onKeyEvent,
+  bool skipTraversal = false,
+  bool canRequestFocus = true,
+}) {
+  return use(
+    _FocusScopeNodeHook(
+      debugLabel: debugLabel,
+      onKey: onKey,
+      onKeyEvent: onKeyEvent,
+      skipTraversal: skipTraversal,
+      canRequestFocus: canRequestFocus,
+    ),
+  );
+}
+
+class _FocusScopeNodeHook extends Hook<FocusScopeNode> {
+  const _FocusScopeNodeHook({
+    this.debugLabel,
+    this.onKey,
+    this.onKeyEvent,
+    required this.skipTraversal,
+    required this.canRequestFocus,
+  });
+
+  final String? debugLabel;
+  final FocusOnKeyCallback? onKey;
+  final FocusOnKeyEventCallback? onKeyEvent;
+  final bool skipTraversal;
+  final bool canRequestFocus;
+
+  @override
+  _FocusScopeNodeHookState createState() {
+    return _FocusScopeNodeHookState();
+  }
+}
+
+class _FocusScopeNodeHookState
+    extends HookState<FocusScopeNode, _FocusScopeNodeHook> {
+  late final FocusScopeNode _focusScopeNode = FocusScopeNode(
+    debugLabel: hook.debugLabel,
+    onKey: hook.onKey,
+    onKeyEvent: hook.onKeyEvent,
+    skipTraversal: hook.skipTraversal,
+    canRequestFocus: hook.canRequestFocus,
+  );
+
+  @override
+  void didUpdateHook(_FocusScopeNodeHook oldHook) {
+    _focusScopeNode
+      ..debugLabel = hook.debugLabel
+      ..skipTraversal = hook.skipTraversal
+      ..canRequestFocus = hook.canRequestFocus
+      ..onKey = hook.onKey
+      ..onKeyEvent = hook.onKeyEvent;
+  }
+
+  @override
+  FocusScopeNode build(BuildContext context) => _focusScopeNode;
+
+  @override
+  void dispose() => _focusScopeNode.dispose();
+
+  @override
+  String get debugLabel => 'useFocusScopeNode';
+}
diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart
index b9152b31..8b8cb7c4 100644
--- a/packages/flutter_hooks/lib/src/hooks.dart
+++ b/packages/flutter_hooks/lib/src/hooks.dart
@@ -14,7 +14,8 @@ part 'misc.dart';
 part 'primitives.dart';
 part 'tab_controller.dart';
 part 'text_controller.dart';
-part 'focus.dart';
+part 'focus_node.dart';
+part 'focus_scope_node.dart';
 part 'scroll_controller.dart';
 part 'page_controller.dart';
 part 'widgets_binding_observer.dart';


Test patch:
diff --git a/packages/flutter_hooks/test/use_focus_scope_node_test.dart b/packages/flutter_hooks/test/use_focus_scope_node_test.dart
new file mode 100644
index 00000000..587b2350
--- /dev/null
+++ b/packages/flutter_hooks/test/use_focus_scope_node_test.dart
@@ -0,0 +1,153 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+import 'mock.dart';
+
+void main() {
+  testWidgets('creates a focus scope node and disposes it', (tester) async {
+    late FocusScopeNode focusScopeNode;
+    await tester.pumpWidget(
+      HookBuilder(builder: (_) {
+        focusScopeNode = useFocusScopeNode();
+        return Container();
+      }),
+    );
+
+    expect(focusScopeNode, isA<FocusScopeNode>());
+    // ignore: invalid_use_of_protected_member
+    expect(focusScopeNode.hasListeners, isFalse);
+
+    final previousValue = focusScopeNode;
+
+    await tester.pumpWidget(
+      HookBuilder(builder: (_) {
+        focusScopeNode = useFocusScopeNode();
+        return Container();
+      }),
+    );
+
+    expect(previousValue, focusScopeNode);
+    // ignore: invalid_use_of_protected_member
+    expect(focusScopeNode.hasListeners, isFalse);
+
+    await tester.pumpWidget(Container());
+
+    expect(
+      () => focusScopeNode.dispose(),
+      throwsAssertionError,
+    );
+  });
+
+  testWidgets('debugFillProperties', (tester) async {
+    await tester.pumpWidget(
+      HookBuilder(builder: (context) {
+        useFocusScopeNode();
+        return const SizedBox();
+      }),
+    );
+
+    final element = tester.element(find.byType(HookBuilder));
+
+    expect(
+      element
+          .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage)
+          .toStringDeep(),
+      equalsIgnoringHashCodes(
+        'HookBuilder\n'
+        ' │ useFocusScopeNode: FocusScopeNode#00000\n'
+        ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n',
+      ),
+    );
+  });
+
+  testWidgets('default values matches with FocusScopeNode', (tester) async {
+    final official = FocusScopeNode();
+
+    late FocusScopeNode focusScopeNode;
+    await tester.pumpWidget(
+      HookBuilder(builder: (_) {
+        focusScopeNode = useFocusScopeNode();
+        return Container();
+      }),
+    );
+
+    expect(focusScopeNode.debugLabel, official.debugLabel);
+    expect(focusScopeNode.onKey, official.onKey);
+    expect(focusScopeNode.skipTraversal, official.skipTraversal);
+    expect(focusScopeNode.canRequestFocus, official.canRequestFocus);
+  });
+
+  testWidgets('has all the FocusScopeNode parameters', (tester) async {
+    KeyEventResult onKey(FocusNode node, RawKeyEvent event) =>
+        KeyEventResult.ignored;
+
+    KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) =>
+        KeyEventResult.ignored;
+
+    late FocusScopeNode focusScopeNode;
+    await tester.pumpWidget(
+      HookBuilder(builder: (_) {
+        focusScopeNode = useFocusScopeNode(
+          debugLabel: 'Foo',
+          onKey: onKey,
+          onKeyEvent: onKeyEvent,
+          skipTraversal: true,
+          canRequestFocus: false,
+        );
+        return Container();
+      }),
+    );
+
+    expect(focusScopeNode.debugLabel, 'Foo');
+    expect(focusScopeNode.onKey, onKey);
+    expect(focusScopeNode.onKeyEvent, onKeyEvent);
+    expect(focusScopeNode.skipTraversal, true);
+    expect(focusScopeNode.canRequestFocus, false);
+  });
+
+  testWidgets('handles parameter change', (tester) async {
+    KeyEventResult onKey(FocusNode node, RawKeyEvent event) =>
+        KeyEventResult.ignored;
+    KeyEventResult onKey2(FocusNode node, RawKeyEvent event) =>
+        KeyEventResult.ignored;
+
+    KeyEventResult onKeyEvent(FocusNode node, KeyEvent event) =>
+        KeyEventResult.ignored;
+    KeyEventResult onKeyEvent2(FocusNode node, KeyEvent event) =>
+        KeyEventResult.ignored;
+
+    late FocusScopeNode focusScopeNode;
+    await tester.pumpWidget(
+      HookBuilder(builder: (_) {
+        focusScopeNode = useFocusScopeNode(
+          debugLabel: 'Foo',
+          onKey: onKey,
+          onKeyEvent: onKeyEvent,
+          skipTraversal: true,
+          canRequestFocus: false,
+        );
+
+        return Container();
+      }),
+    );
+
+    await tester.pumpWidget(
+      HookBuilder(builder: (_) {
+        focusScopeNode = useFocusScopeNode(
+          debugLabel: 'Bar',
+          onKey: onKey2,
+          onKeyEvent: onKeyEvent2,
+        );
+
+        return Container();
+      }),
+    );
+
+    expect(focusScopeNode.onKey, onKey2);
+    expect(focusScopeNode.onKeyEvent, onKeyEvent2);
+    expect(focusScopeNode.debugLabel, 'Bar');
+    expect(focusScopeNode.skipTraversal, false);
+    expect(focusScopeNode.canRequestFocus, true);
+  });
+}