diff --git a/packages/app-harness/src/screens/tests/viewGroup.tsx b/packages/app-harness/src/screens/tests/viewGroup.tsx
index 55d7e749..d9318ebc 100644
--- a/packages/app-harness/src/screens/tests/viewGroup.tsx
+++ b/packages/app-harness/src/screens/tests/viewGroup.tsx
@@ -11,7 +11,12 @@ const ViewGroup = () => {
+
+
);
};
diff --git a/packages/create/src/focusManager/model/view.ts b/packages/create/src/focusManager/model/view.ts
index 4cd23fdf..006a9522 100644
--- a/packages/create/src/focusManager/model/view.ts
+++ b/packages/create/src/focusManager/model/view.ts
@@ -197,6 +197,17 @@ class View extends FocusModel {
}
}
+ if (value) {
+ let parent = this.getParent();
+ while (parent) {
+ if (parent instanceof ViewGroup) {
+ parent.setCurrentFocus(this);
+ break;
+ }
+ parent = parent?.getParent();
+ }
+ }
+
return this;
}
@@ -261,7 +272,7 @@ class View extends FocusModel {
let group;
while (parent) {
- if (parent instanceof ViewGroup) {
+ if (parent instanceof ViewGroup && !parent.isFocusAllowedOutsideGroup()) {
group = parent.getGroup();
parent = null;
} else {
diff --git a/packages/create/src/focusManager/model/viewGroup.ts b/packages/create/src/focusManager/model/viewGroup.ts
index 4daaf509..6a6d1c46 100644
--- a/packages/create/src/focusManager/model/viewGroup.ts
+++ b/packages/create/src/focusManager/model/viewGroup.ts
@@ -1,18 +1,21 @@
-import { View } from 'react-native';
-import FocusModel from './abstractFocusModel';
+import FocusModel, { MODEL_TYPES } from './abstractFocusModel';
import Event, { EVENT_TYPES } from '../events';
import { CoreManager } from '../..';
import { MutableRefObject } from 'react';
import { ViewGroupProps } from '../types';
+import View from './view';
+import RecyclerView from './recycler';
class ViewGroup extends FocusModel {
private _group?: string;
private _focusKey?: string;
+ private _currentFocus: View | null = null;
+ private _allowFocusOutsideGroup = false;
constructor(params: Omit) {
super(params);
- const { focusContext, group, focusKey } = params;
+ const { focusContext, group, focusKey, allowFocusOutsideGroup = false } = params;
const id = CoreManager.generateID(8);
this._id = focusContext?.getId() ? `${focusContext.getId()}:viewGroup-${id}` : `viewGroup-${id}`;
@@ -20,6 +23,7 @@ class ViewGroup extends FocusModel {
this._type = 'viewGroup';
this._group = group;
this._focusKey = focusKey;
+ this._allowFocusOutsideGroup = allowFocusOutsideGroup;
this._onMount = this._onMount.bind(this);
this._onUnmount = this._onUnmount.bind(this);
@@ -48,6 +52,24 @@ class ViewGroup extends FocusModel {
// END EVENTS
+ public getFirstFocusableInViewGroup = (): View | null => {
+ if (CoreManager.isFocusManagerEnabled()) {
+ if (this._currentFocus) return this._currentFocus;
+
+ const firstChildren = this._children.find((ch) => [MODEL_TYPES.ROW, MODEL_TYPES.GRID, MODEL_TYPES.VIEW].includes(ch.getType() as never))
+
+ if (firstChildren && firstChildren.getType() === MODEL_TYPES.VIEW) {
+ return firstChildren as View;
+ } else if (firstChildren) {
+ const recycler = firstChildren as RecyclerView;
+ if (recycler.getFocusedView()) return recycler.getFocusedView();
+ return recycler.getChildren()[0] as View | null;
+ }
+ }
+
+ return null;
+ };
+
public getGroup() {
return this._group;
}
@@ -71,6 +93,21 @@ class ViewGroup extends FocusModel {
public getNode(): MutableRefObject {
return this.node;
}
+
+ public setCurrentFocus(model: View | null): this {
+ this._currentFocus = model;
+
+ return this;
+ }
+
+ public getCurrentFocus(): View | null {
+ return this._currentFocus;
+ }
+
+ public isFocusAllowedOutsideGroup(): boolean {
+ return this._allowFocusOutsideGroup;
+ }
+
}
export default ViewGroup;
diff --git a/packages/create/src/focusManager/service/core.ts b/packages/create/src/focusManager/service/core.ts
index 6333ead6..b29ccbd6 100644
--- a/packages/create/src/focusManager/service/core.ts
+++ b/packages/create/src/focusManager/service/core.ts
@@ -5,11 +5,12 @@ import Scroller from './scroller';
import Logger from './logger';
import FocusModel, { MODEL_TYPES } from '../model/abstractFocusModel';
import { DIRECTIONS } from '../constants';
-import { ClosestNodeOutput, FocusDirection, ScreenType, ViewType } from '../types';
+import { ClosestNodeOutput, FocusDirection, ScreenType, ViewType, ViewGroupType } from '../types';
class CoreManager {
private _focusAwareElements: Record = {};
private _views: Record = {};
+ private _viewGroups: Record = {};
private _screens: Record = {};
private _currentFocus: ViewType | null = null;
private _debuggerEnabled = false;
@@ -60,6 +61,8 @@ class CoreManager {
this._screens[model.getId()] = model as ScreenType;
} else if (model.getType() === MODEL_TYPES.VIEW) {
this._views[model.getId()] = model as ViewType;
+ } else if (model.getType() === MODEL_TYPES.VIEW_GROUP) {
+ this._viewGroups[model.getId()] = model as ViewGroupType;
}
Object.keys(this._focusAwareElements).forEach((k) => {
@@ -150,8 +153,21 @@ class CoreManager {
const screen = Object.values(this._screens).find(
(model) => model.getFocusKey() === focusKey && model.isInForeground()
);
+
if (screen) {
screen.setFocus(screen.getFirstFocusableOnScreen());
+ return;
+ }
+
+ const viewGroup = Object.values(this._viewGroups).find(
+ (model) => model.getFocusKey() === focusKey && model.isInForeground()
+ );
+
+ if (viewGroup) {
+ const element = viewGroup.getFirstFocusableInViewGroup();
+ if (element) {
+ this.executeFocus(element);
+ }
}
}
};
@@ -310,7 +326,7 @@ class CoreManager {
public pickActiveForcedFocusContext(nextForcedFocusKey: string | string[]): string | null {
const isActive = (focusKey: string) =>
- Object.values({ ...this._views, ...this._screens }).find(
+ Object.values({ ...this._views, ...this._screens, ...this._viewGroups }).find(
(model) => model.getFocusKey() === focusKey && model.isInForeground()
);
diff --git a/packages/create/src/focusManager/types.ts b/packages/create/src/focusManager/types.ts
index ec8391ec..fb997f79 100644
--- a/packages/create/src/focusManager/types.ts
+++ b/packages/create/src/focusManager/types.ts
@@ -17,6 +17,7 @@ import View from './model/view';
import { SCREEN_STATES, VIEWPORT_ALIGNMENT } from './model/screen';
import Screen from './model/screen';
import { DIRECTIONS } from './constants';
+import ViewGroup from './model/viewGroup';
export type FocusDirection = typeof DIRECTIONS[keyof typeof DIRECTIONS];
export type WindowAlignment = 'both-edge' | 'low-edge';
@@ -24,6 +25,7 @@ export type ScreenStates = 'background' | 'foreground';
export type FocusContext = FocusModel;
export type ScreenType = Screen;
export type ViewType = View;
+export type ViewGroupType = ViewGroup;
type AnimatorTypeScale = 'scale';
type AnimatorTypeScaleWithBorder = 'scale_with_border';
type AnimatorTypeAnimatorBorder = 'border';
@@ -132,6 +134,7 @@ export interface ViewProps extends RNViewProps, MouseEvents {
nextFocusRight?: string | string[];
nextFocusUp?: string | string[];
nextFocusDown?: string | string[];
+ allowFocusOutsideGroup?: boolean;
};
focusContext?: FocusModel;
focusRepeatContext?: CreateListRenderItemInfo['focusRepeatContext'];
@@ -146,6 +149,7 @@ export interface ViewGroupProps extends RNViewProps {
nextFocusRight?: string | string[];
nextFocusUp?: string | string[];
nextFocusDown?: string | string[];
+ allowFocusOutsideGroup?: boolean;
};
focusContext?: FocusContext;
ref?: React.ForwardedRef | React.MutableRefObject;