Focus Management
Focus management is essential for building accessible and user-friendly TV, set-top box, and remote-driven applications. Hosanna UI's focus system ensures that users can navigate your app smoothly using a remote or keyboard, with clear visual feedback and predictable behavior.
-
What is Focus? Focus determines which UI element is currently active and ready to receive input. Only one element can be focused at a time, and navigation keys (arrows, OK, etc.) move focus between elements.
-
Why is Focus Management Important? Good focus management improves usability, accessibility, and user satisfaction, especially on devices without touch or mouse input. It also enables features like focus rings, keyboard shortcuts, and modal dialogs.
-
How Hosanna UI Helps: Hosanna UI provides:
- Automatic focus navigation between controls
- Customizable focus logic for advanced layouts
- Visual focus indicators (focus rectangles)
- Support for focus locking and modal dialogs
- Debug tools for visualizing and troubleshooting focus
Focus Resolution Flow
How focus moves on key presses and state changes.
Begin with focusMap
and isInitialFocus
. Only override hooks when you need special rules.
Auto focus resolution and hooks
Focus in Hosanna is resolved automatically wherever possible, and can be guided declaratively or imperatively.
- Auto initial focus: Set
isInitialFocus: true
on a subview to receive focus when the parent is focused. - Declarative focus map: Provide
focusMap
on any view to steer directional navigation. - Programmatic helpers: Override view hooks or call helper methods where needed.
Key hooks and helpers:
/**
* Default animation provider mapping for simple properties.
* By default, maps 'opacity' to the base renderer's 'opacity' field.
* Views can override to provide richer mappings.
*/
getAnimationPropsForStateProperty(prop: string): AnimationFieldBinding[] | undefined {
if (this.renderer) {
return [{ renderer: this.renderer, field: prop }];
}
return [];
}
/** Convenience: animate logical properties across this view's subtree. */
animateViews(anims: AnimationSpec, duration: number): HosannaViewAnimator {
return HosannaViewAnimator.animateViews(this as unknown as BaseView<ViewState>, anims, duration);
}
Using onFocus and onBlur
Override hooks to react to focus enter/exit. The onFocus(event)
callback provides the ability to steer focus (e.g., set event.nextFocusId
).
@view('SampleFocusable')
export class SampleFocusableView extends BaseExampleScreenView<ViewState> {
protected override onFocus(event: FocusEvent<ViewState>) {
console.info('Focused:', this.id);
// Example: force the next focus to a particular subview when pressing Right
if (event.direction === Direction.Right) {
event.nextFocusId = 'ok';
}
}
protected override onBlur() {
console.info('Blurred:', this.id);
}
}
Initial focus resolution helpers:
protected getInitialFocusId() {
if (this.initialFocusId) {
return this.initialFocusId;
} else if (this.children[0]?.id !== undefined) {
return this.children[0]?.id;
} else if (this.children[0]?.pendingState.id !== undefined) {
return this.children[0]?.pendingState.id;
}
return undefined;
}
protected setFocusedSubview(id: string) {
if (id) {
const child = this.getViewById(id);
if (child) {
if (this.isInFocusChain()) {
this.focusManager.setFocus(child);
} else {
this.focusedChildId = id;
}
}
}
}
Restore focus helpers that can be overridden:
protected restoreFocus(nextFocusId: string) {
if (nextFocusId) {
let child = this.getViewById(nextFocusId) as IHosannaView<ViewState>;
if (child !== undefined && child.canReceiveFocus === true && child.visible === true) {
this.focusManager.setFocus(child)
} else {
child = this.findNextFocusable({ direction: Direction.None, originView: this, previousView: this }) as IHosannaView<ViewState>;
if (child) {
this.focusManager.setFocus(child);
return child;
} else {
for (const subView of this.subViews) {
if (subView.canReceiveFocus && subView.visible) {
this.focusManager.setFocus(subView);
return subView;
}
}
}
}
}
}
Utilities to restore focus to the last focused child:
protected restoreFocusToFocusedChildId() {
if (this.focusedChildId) {
const child = this.getViewById(this.focusedChildId) as IHosannaView<ViewState>;
if (child !== undefined && child.canReceiveFocus && child.visible) {
this.focusManager.setFocus(child);
return true;
}
}
return false;
}
Directional resolution with NextViewFocus
and focusMap
:
export interface ViewFocusMap {
[key: string]: string | NextViewFocus | undefined;
}
export enum NextViewFocus {
Maintain = 'maintain',
Exit = 'exit',
None = 'none',
}
Declarative Focus Maps
Use focusMap
to steer directional navigation with IDs or NextViewFocus
tokens.
VGroup([
Button({ id: 'ok', text: 'OK' }),
Button({ id: 'cancel', text: 'Cancel' })
])
.onFocus((e) => { e.nextFocusId = 'ok'; })
.focusMap({ left: 'cancel', right: 'ok', default: NextViewFocus.Maintain });
Another example mapping a grid of items:
GridGroup({ id: 'grid', numCols: 3 }, items)
.focusMap({
// Keep focus within the grid by default
default: NextViewFocus.Maintain,
// Jump to toolbar when pressing Up from the first row
up: 'toolbar',
});
Override hooks:
onFocus(event)
andonBlur()
can be overridden to react to focus entry/exit.findNextFocusable(event)
can be implemented to programmatically choose the next view.getViewToRestoreFocusTo()
can be overridden to customize fallback restoration.
FocusManager Class
The core of focus handling is the FocusManager
class, which:
- Manages which view is currently focused
- Handles key events (arrows, OK, back, etc.) to move focus
- Supports focus locking and custom navigation
- Provides debug utilities for visualizing focus
Key Features
- Automatic Focus Navigation: Moves focus between focusable elements using remote or keyboard input.
- Custom Focus Logic: Override or extend navigation by providing custom handlers.
- Focus Locking: Temporarily lock focus to a specific view (e.g., modal dialogs).
- Debug View: Visualize focus transitions and current focus state for development.
Focus Classes Overview
How the main classes relate.
FocusManager
: global navigation and focus stateBaseView
: implements hooks and helpersViewFocusMap
: declarative directional rules per view
- Automatic Focus Navigation: Moves focus between focusable elements using remote or keyboard input.
- Custom Focus Logic: Override or extend navigation by providing custom handlers.
- Focus Locking: Temporarily lock focus to a specific view (e.g., modal dialogs).
- Debug View: Visualize focus transitions and current focus state for development.
Example: Basic Focus Navigation
import { FocusManager } from 'hosanna-ui/hosanna-bridge-lib/FocusManager';
const focusManager = new FocusManager();
// Set initial focusocusManager.setFocus(myButton);
// Handle key events (e.g., from remote)
document.addEventListener('keydown', (e) => {
focusManager.onKeyEvent(e.key, true);
});
Example: Locking Focus
// Lock focus to a dialog
focusManager._setFocusLocked(myDialog);
// Unlock focus when done
focusManager._setFocusUnlocked();
Example: Adding a Debug View
focusManager.addDebugView(); // Shows a visual overlay for focus
CollectionViewFocusManager
For list/grid navigation, CollectionViewFocusManager
manages the focus rectangle and smooth transitions between items.
import { CollectionViewFocusManager } from 'hosanna-ui/hosanna-list/CollectionViewFocusManager';
const collectionFocusManager = new CollectionViewFocusManager(myCollectionView);
collectionFocusManager.setNextFocusInfo({
rowIndex: 0,
itemIndex: 1,
offset: [0,0],
size: [200,80],
imageUri: 'focusrect.9.png',
color: '#fff',
feedbackOffsets: [0,0,0,0],
displayMode: 1,
});
collectionFocusManager.renderCurrentFocusInfo();
For advanced scenarios, see the API docs for FocusManager
, CollectionViewFocusManager
, and related interfaces.