giggles 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -53,8 +53,9 @@ type RouterProps = {
53
53
  children: React__default.ReactNode;
54
54
  initialScreen: string;
55
55
  initialParams?: Record<string, unknown>;
56
+ restoreFocus?: boolean;
56
57
  };
57
- declare function Router({ children, initialScreen, initialParams }: RouterProps): react_jsx_runtime.JSX.Element;
58
+ declare function Router({ children, initialScreen, initialParams, restoreFocus }: RouterProps): react_jsx_runtime.JSX.Element;
58
59
 
59
60
  type ScreenProps = {
60
61
  name: string;
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { jsx } from "react/jsx-runtime";
12
12
  var FocusContext = createContext(null);
13
13
  var FocusProvider = ({ children }) => {
14
14
  const nodesRef = useRef(/* @__PURE__ */ new Map());
15
+ const pendingFocusFirstChildRef = useRef(/* @__PURE__ */ new Set());
15
16
  const [focusedId, setFocusedId] = useState(null);
16
17
  const [activeBranchNodes, setActiveBranchNodes] = useState(/* @__PURE__ */ new Set());
17
18
  const [activeBranchPath, setActiveBranchPath] = useState([]);
@@ -34,6 +35,18 @@ var FocusProvider = ({ children }) => {
34
35
  return id;
35
36
  });
36
37
  }, []);
38
+ const focusFirstChild = useCallback(
39
+ (parentId) => {
40
+ const nodes = nodesRef.current;
41
+ const parent = nodes.get(parentId);
42
+ if (parent && parent.childrenIds.length > 0) {
43
+ focusNode(parent.childrenIds[0]);
44
+ } else {
45
+ pendingFocusFirstChildRef.current.add(parentId);
46
+ }
47
+ },
48
+ [focusNode]
49
+ );
37
50
  const registerNode = useCallback(
38
51
  (id, parentId) => {
39
52
  const nodes = nodesRef.current;
@@ -46,7 +59,12 @@ var FocusProvider = ({ children }) => {
46
59
  if (parentId) {
47
60
  const parent = nodes.get(parentId);
48
61
  if (parent && !parent.childrenIds.includes(id)) {
62
+ const wasEmpty = parent.childrenIds.length === 0;
49
63
  parent.childrenIds.push(id);
64
+ if (wasEmpty && pendingFocusFirstChildRef.current.has(parentId)) {
65
+ pendingFocusFirstChildRef.current.delete(parentId);
66
+ focusNode(id);
67
+ }
50
68
  }
51
69
  }
52
70
  nodes.forEach((existingNode) => {
@@ -71,6 +89,7 @@ var FocusProvider = ({ children }) => {
71
89
  }
72
90
  }
73
91
  nodes.delete(id);
92
+ pendingFocusFirstChildRef.current.delete(id);
74
93
  setFocusedId((current) => {
75
94
  if (current !== id) return current;
76
95
  if (node.parentId) {
@@ -139,6 +158,7 @@ var FocusProvider = ({ children }) => {
139
158
  registerNode,
140
159
  unregisterNode,
141
160
  focusNode,
161
+ focusFirstChild,
142
162
  isFocused,
143
163
  getFocusedId,
144
164
  isInActiveBranch,
@@ -401,7 +421,7 @@ function GigglesProvider({ children }) {
401
421
  }
402
422
 
403
423
  // src/core/router/Router.tsx
404
- import React4, { useCallback as useCallback4, useReducer, useRef as useRef4 } from "react";
424
+ import React4, { useCallback as useCallback4, useReducer, useRef as useRef5 } from "react";
405
425
 
406
426
  // src/core/router/Screen.tsx
407
427
  function Screen(_props) {
@@ -409,7 +429,7 @@ function Screen(_props) {
409
429
  }
410
430
 
411
431
  // src/core/router/ScreenEntry.tsx
412
- import { useMemo as useMemo2 } from "react";
432
+ import React3, { useEffect as useEffect5, useId as useId2, useMemo as useMemo2, useRef as useRef4 } from "react";
413
433
  import { Box } from "ink";
414
434
 
415
435
  // src/core/router/NavigationContext.tsx
@@ -429,17 +449,44 @@ function ScreenEntry({
429
449
  entry,
430
450
  isTop,
431
451
  canGoBack,
452
+ restoreFocus,
432
453
  component: Component,
433
454
  push,
434
455
  pop,
435
456
  replace,
436
457
  reset
437
458
  }) {
459
+ const screenNodeId = useId2();
460
+ const parentId = React3.useContext(FocusNodeContext);
461
+ const { registerNode, unregisterNode, focusFirstChild, focusNode, getFocusedId } = useFocusContext();
462
+ const lastFocusedChildRef = useRef4(null);
463
+ const wasTopRef = useRef4(isTop);
464
+ useEffect5(() => {
465
+ registerNode(screenNodeId, parentId);
466
+ return () => {
467
+ unregisterNode(screenNodeId);
468
+ };
469
+ }, [screenNodeId, parentId, registerNode, unregisterNode]);
470
+ useEffect5(() => {
471
+ if (!wasTopRef.current && isTop) {
472
+ const saved = restoreFocus ? lastFocusedChildRef.current : null;
473
+ if (saved) {
474
+ focusNode(saved);
475
+ } else {
476
+ focusFirstChild(screenNodeId);
477
+ }
478
+ } else if (wasTopRef.current && !isTop) {
479
+ lastFocusedChildRef.current = getFocusedId();
480
+ } else if (isTop) {
481
+ focusFirstChild(screenNodeId);
482
+ }
483
+ wasTopRef.current = isTop;
484
+ }, [isTop, screenNodeId, restoreFocus, focusFirstChild, focusNode, getFocusedId]);
438
485
  const value = useMemo2(
439
486
  () => ({ currentRoute: entry, active: isTop, canGoBack, push, pop, replace, reset }),
440
487
  [entry, isTop, canGoBack, push, pop, replace, reset]
441
488
  );
442
- return /* @__PURE__ */ jsx7(NavigationContext.Provider, { value, children: /* @__PURE__ */ jsx7(Box, { display: isTop ? "flex" : "none", children: /* @__PURE__ */ jsx7(Component, { ...entry.params }) }) });
489
+ return /* @__PURE__ */ jsx7(NavigationContext.Provider, { value, children: /* @__PURE__ */ jsx7(FocusNodeContext.Provider, { value: screenNodeId, children: /* @__PURE__ */ jsx7(Box, { display: isTop ? "flex" : "none", children: /* @__PURE__ */ jsx7(Component, { ...entry.params }) }) }) });
443
490
  }
444
491
 
445
492
  // src/core/router/Router.tsx
@@ -456,10 +503,10 @@ function routerReducer(stack, action) {
456
503
  return [action.route];
457
504
  }
458
505
  }
459
- function Router({ children, initialScreen, initialParams }) {
460
- const screenId = useRef4(0);
506
+ function Router({ children, initialScreen, initialParams, restoreFocus = true }) {
507
+ const screenId = useRef5(0);
461
508
  const routes = React4.Children.toArray(children).filter((child) => React4.isValidElement(child) && child.type === Screen).map((child) => child.props);
462
- const screenNamesRef = useRef4(/* @__PURE__ */ new Set());
509
+ const screenNamesRef = useRef5(/* @__PURE__ */ new Set());
463
510
  screenNamesRef.current = new Set(routes.map((r) => r.name));
464
511
  const assertScreen = useCallback4((name) => {
465
512
  if (!screenNamesRef.current.has(name)) {
@@ -514,6 +561,7 @@ function Router({ children, initialScreen, initialParams }) {
514
561
  entry,
515
562
  isTop: i === stack.length - 1,
516
563
  canGoBack,
564
+ restoreFocus,
517
565
  component: Component,
518
566
  push,
519
567
  pop,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",